diff --git a/packages/app/auth/user.ts b/packages/app/auth/user.ts index e43a01859..252134f3a 100644 --- a/packages/app/auth/user.ts +++ b/packages/app/auth/user.ts @@ -6,8 +6,10 @@ import { authOptions } from 'pages/api/auth/[...nextauth]' import { getServerSession } from 'next-auth' import { UserWithRoles } from './types' -export async function getLoggedInUser(req: any, res: any): Promise { - // TODO: figure out correct typings +export async function getLoggedInUser( + req: any, + res: any +): Promise { const session = await getServerSession(req, res, authOptions) if (!session?.user?.uid) { diff --git a/packages/app/declarations.d.ts b/packages/app/declarations.d.ts index 1814c7b5f..2595bfdae 100644 --- a/packages/app/declarations.d.ts +++ b/packages/app/declarations.d.ts @@ -1,9 +1,9 @@ +import { DetailedHTMLProps, HTMLAttributes } from 'react' +import { HttpError } from 'http-errors' import { JSX as LocalJSX } from '@gesdisc/meditor-components/loader' +import 'next-auth' import type { CKEditor } from 'ckeditor4-react' -import { DetailedHTMLProps, HTMLAttributes } from 'react' -import type { HttpException } from './utils/errors' import type { Stan } from 'node-nats-streaming' -import 'next-auth' import type { User as NextAuthUser } from 'next-auth' // Read more at: https://next-auth.js.org/getting-started/typescript#module-augmentation @@ -30,7 +30,7 @@ export type APIError = { error: string } -export type ErrorData = [Error | HttpException | null, T | null] +export type ErrorData = [Error | HttpError | null, T | null] type gReactProps = { [P in keyof T]?: DetailedHTMLProps, T[P]> diff --git a/packages/app/lib/with-api-error-handler.ts b/packages/app/lib/with-api-error-handler.ts new file mode 100644 index 000000000..1a0f929b5 --- /dev/null +++ b/packages/app/lib/with-api-error-handler.ts @@ -0,0 +1,65 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { ZodError } from 'zod' +import createError from 'http-errors' +import { parameterWithInflection } from './grammar' +import log from './log' + +export function withApiErrorHandler(handler) { + return async (req: NextApiRequest, res: NextApiResponse) => { + try { + // Call the original handler + await handler(req, res) + } catch (err) { + // Any unhandled exceptions will be caught and reported as a clean JSON response + return apiError(err, res) + } + } +} + +/** + * converts errors to a JSON api response + * To prevent leaking implementation details to an end-user, if the error isn't an instance of HttpError, only return a generic error. + */ +export function apiError( + error: Error | ZodError | createError.HttpError, + response: NextApiResponse +) { + let interstitialError = error + + if (error.name === 'ZodError') { + interstitialError = formatZodError(error as ZodError) + } + + const safeError = + interstitialError instanceof createError.HttpError + ? interstitialError + : new createError.InternalServerError() + + if (safeError instanceof createError.InternalServerError) { + // this is an unknown error, lets dump out the details so we can debug it later + log.error(safeError) + } + + return response.status(safeError.status).json({ + status: safeError.status, + error: safeError.message, + }) +} + +export function formatZodError(error: ZodError, messagePrefix?: string) { + const errorString = error.issues.reduce((accumulator, current, index, self) => { + //* We want spaces between errors but not for the last error. + const maybeSpace = index + 1 === self.length ? '' : ' ' + + accumulator += `${ + messagePrefix ?? + `For query ${parameterWithInflection( + current.path.length + )} ${current.path.toString()}: ` + }${current.message}.${maybeSpace}` + + return accumulator + }, ``) + + return new createError.BadRequest(errorString) +} diff --git a/packages/app/middleware.ts b/packages/app/middleware.ts new file mode 100644 index 000000000..d9350f2fb --- /dev/null +++ b/packages/app/middleware.ts @@ -0,0 +1,16 @@ +export { default } from 'next-auth/middleware' + +export const config = { + matcher: [ + /* + * Require authentication for all request paths except for the ones starting with: + * - installation + * - signin (mEditor's sign in page) + * - api (API routes) + * - _next (NextJS static files) + * - images (/public/images static images) + * - favicon.ico + */ + '/((?!installation|signin|api|_next|images|favicon.ico).*)', + ], +} diff --git a/packages/app/models/service.ts b/packages/app/models/service.ts index df74c2e78..1a2ee22db 100644 --- a/packages/app/models/service.ts +++ b/packages/app/models/service.ts @@ -1,12 +1,12 @@ import jsonpath from 'jsonpath' -import type { ErrorData } from '../declarations' -import { getDocumentsDb } from '../documents/db' import log from '../lib/log' -import { runModelTemplates } from '../macros/service' import { ErrorCode, HttpException } from '../utils/errors' -import { isJson } from '../utils/jsonschema-validate' -import { getWorkflowByDocumentState } from '../workflows/service' +import { getDocumentsDb } from '../documents/db' import { getModelsDb } from './db' +import { getWorkflowByDocumentState } from '../workflows/service' +import { isJson } from '../utils/jsonschema-validate' +import { runModelTemplates } from '../macros/service' +import type { ErrorData } from '../declarations' import type { Model, ModelWithWorkflow } from './types' import type { UserWithRoles } from 'auth/types' @@ -203,7 +203,8 @@ export async function getModelsWithDocumentCount(): Promise> /** * if user is not authenticated, verify the requested model is not in the list of models requiring authentication + * this was a mEditor design decision early on to allow anonymous access to most documents */ -export function userCanAccessModel(modelName: string, user: UserWithRoles) { - return !!user?.uid || !MODELS_REQUIRING_AUTHENTICATION.includes(modelName) +export async function userCanAccessModel(user: UserWithRoles, modelName: string) { + return user?.uid ? true : !MODELS_REQUIRING_AUTHENTICATION.includes(modelName) } diff --git a/packages/app/package-lock.json b/packages/app/package-lock.json index c52b4210a..cb6f93fc9 100644 --- a/packages/app/package-lock.json +++ b/packages/app/package-lock.json @@ -25,6 +25,7 @@ "eslint-config-next": "^15.0.3", "fuse.js": "^6.6.2", "he": "^1.2.0", + "http-errors": "^2.0.0", "immer": "^9.0.6", "immutable-json-patch": "^6.0.1", "isomorphic-unfetch": "^3.0.0", @@ -5262,6 +5263,14 @@ "node": ">=0.10" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -7057,6 +7066,21 @@ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -10710,6 +10734,11 @@ "node": ">= 0.4" } }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, "node_modules/sharp": { "version": "0.33.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", @@ -10940,6 +10969,14 @@ "escodegen": "^1.8.1" } }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/streamroller": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.3.tgz", @@ -11310,6 +11347,14 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, "node_modules/touch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", diff --git a/packages/app/package.json b/packages/app/package.json index ca8d8059c..f0d659253 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -30,6 +30,7 @@ "eslint-config-next": "^15.0.3", "fuse.js": "^6.6.2", "he": "^1.2.0", + "http-errors": "^2.0.0", "immer": "^9.0.6", "immutable-json-patch": "^6.0.1", "isomorphic-unfetch": "^3.0.0", diff --git a/packages/app/pages/api/admin/index.ts b/packages/app/pages/api/admin/index.ts index a98b8f9fe..909d434c1 100644 --- a/packages/app/pages/api/admin/index.ts +++ b/packages/app/pages/api/admin/index.ts @@ -1,8 +1,9 @@ +import { withApiErrorHandler } from 'lib/with-api-error-handler' import type { NextApiRequest, NextApiResponse } from 'next' +import createError from 'http-errors' -export default async function handler(_req: NextApiRequest, res: NextApiResponse) { - // TODO: implement - res.status(501).json({ - message: 'Not Implemented', - }) +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + throw createError.NotImplemented() } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/admin/seed-db/index.ts b/packages/app/pages/api/admin/seed-db/index.ts index d9629c737..1c9b23e4e 100644 --- a/packages/app/pages/api/admin/seed-db/index.ts +++ b/packages/app/pages/api/admin/seed-db/index.ts @@ -1,20 +1,19 @@ import type { NextApiRequest, NextApiResponse } from 'next' import { setUpNewInstallation } from '../../../../setup/service' -import { apiError } from '../../../../utils/errors' +import { withApiErrorHandler } from 'lib/with-api-error-handler' +import assert from 'assert' +import createError from 'http-errors' -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - switch (req.method) { - case 'POST': { - const [error] = await setUpNewInstallation(req.body) +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + assert(req.method === 'POST', new createError.MethodNotAllowed()) - if (!!error) { - return apiError(error, res) - } + const [error] = await setUpNewInstallation(req.body) - return res.status(204).end() - } - - default: - return res.status(405).end() + if (!!error) { + throw error } + + return res.status(204).end() } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/health/index.ts b/packages/app/pages/api/health/index.ts index 1c61ce745..a47d8b8a4 100644 --- a/packages/app/pages/api/health/index.ts +++ b/packages/app/pages/api/health/index.ts @@ -1,3 +1,4 @@ +import { withApiErrorHandler } from 'lib/with-api-error-handler' import type { NextApiRequest, NextApiResponse } from 'next' const healthcheck = { @@ -8,7 +9,7 @@ const healthcheck = { }, } -export default async function handler(_req: NextApiRequest, res: NextApiResponse) { +const handler = async (req: NextApiRequest, res: NextApiResponse) => { //timeout for fetch request sice meditor_notifier is an internal service const controller = new AbortController() @@ -33,3 +34,5 @@ export default async function handler(_req: NextApiRequest, res: NextApiResponse res.status(500).json({ message: 'Meditor API is not healthy', err }) } } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/legacy-endpoints/changeDocumentState.ts b/packages/app/pages/api/legacy-endpoints/changeDocumentState.ts index 0cac308fe..f8d5971d6 100644 --- a/packages/app/pages/api/legacy-endpoints/changeDocumentState.ts +++ b/packages/app/pages/api/legacy-endpoints/changeDocumentState.ts @@ -1,8 +1,8 @@ import type { NextApiRequest, NextApiResponse } from 'next' -import { apiError } from 'utils/errors' import changeDocumentStateHandler from '../models/[modelName]/documents/[documentTitle]/change-document-state' import { baseDocumentSchema } from './_schemas' import { z } from 'zod' +import { withApiErrorHandler } from 'lib/with-api-error-handler' const schema = baseDocumentSchema .extend({ @@ -14,14 +14,12 @@ const schema = baseDocumentSchema state, })) -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - // replaces query params with params mapped to RESTful names (e.g. "model" -> "modelName", etc.) - req.query = schema.parse(req.query) - req.method = req.method === 'GET' ? 'POST' : req.method // old API used GET, new API uses POST +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + // replaces query params with params mapped to RESTful names (e.g. "model" -> "modelName", etc.) + req.query = schema.parse(req.query) + req.method = req.method === 'GET' ? 'POST' : req.method // old API used GET, new API uses POST - return changeDocumentStateHandler(req, res) - } catch (err) { - return apiError(err, res) - } + return changeDocumentStateHandler(req, res) } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/legacy-endpoints/cloneDocument.ts b/packages/app/pages/api/legacy-endpoints/cloneDocument.ts index 8fd64db9b..573af1493 100644 --- a/packages/app/pages/api/legacy-endpoints/cloneDocument.ts +++ b/packages/app/pages/api/legacy-endpoints/cloneDocument.ts @@ -1,8 +1,8 @@ import type { NextApiRequest, NextApiResponse } from 'next' -import { apiError } from 'utils/errors' import cloneDocumentHandler from '../models/[modelName]/documents/[documentTitle]/clone-document' import { baseDocumentSchema } from './_schemas' import { z } from 'zod' +import { withApiErrorHandler } from 'lib/with-api-error-handler' const schema = baseDocumentSchema .extend({ @@ -14,13 +14,11 @@ const schema = baseDocumentSchema newTitle, })) -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - // replaces query params with params mapped to RESTful names (e.g. "model" -> "modelName", etc.) - req.query = schema.parse(req.query) +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + // replaces query params with params mapped to RESTful names (e.g. "model" -> "modelName", etc.) + req.query = schema.parse(req.query) - return cloneDocumentHandler(req, res) - } catch (err) { - return apiError(err, res) - } + return cloneDocumentHandler(req, res) } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/legacy-endpoints/getComments.ts b/packages/app/pages/api/legacy-endpoints/getComments.ts index 11f14e4fc..1970e0191 100644 --- a/packages/app/pages/api/legacy-endpoints/getComments.ts +++ b/packages/app/pages/api/legacy-endpoints/getComments.ts @@ -1,15 +1,13 @@ import type { NextApiRequest, NextApiResponse } from 'next' -import { apiError } from 'utils/errors' import getCommentsHandler from '../models/[modelName]/documents/[documentTitle]/comments/' import { documentSchema } from './_schemas' +import { withApiErrorHandler } from 'lib/with-api-error-handler' -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - // replaces query params with params mapped to RESTful names (e.g. "model" -> "modelName", etc.) - req.query = documentSchema.parse(req.query) +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + // replaces query params with params mapped to RESTful names (e.g. "model" -> "modelName", etc.) + req.query = documentSchema.parse(req.query) - return getCommentsHandler(req, res) - } catch (err) { - return apiError(err, res) - } + return getCommentsHandler(req, res) } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/legacy-endpoints/getDocument.ts b/packages/app/pages/api/legacy-endpoints/getDocument.ts index 1adae0a98..b227cd972 100644 --- a/packages/app/pages/api/legacy-endpoints/getDocument.ts +++ b/packages/app/pages/api/legacy-endpoints/getDocument.ts @@ -1,18 +1,16 @@ import type { NextApiRequest, NextApiResponse } from 'next' -import { apiError } from 'utils/errors' import { default as getDocumentHandler } from '../models/[modelName]/documents/[documentTitle]/' import { default as getDocumentWithVersionHandler } from '../models/[modelName]/documents/[documentTitle]/' import { documentSchema } from './_schemas' +import { withApiErrorHandler } from 'lib/with-api-error-handler' -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - // replaces query params with params mapped to RESTful names (e.g. "model" -> "modelName", etc.) - req.query = documentSchema.parse(req.query) +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + // replaces query params with params mapped to RESTful names (e.g. "model" -> "modelName", etc.) + req.query = documentSchema.parse(req.query) - return req.query.version - ? getDocumentWithVersionHandler(req, res) - : getDocumentHandler(req, res) - } catch (err) { - return apiError(err, res) - } + return req.query.version + ? getDocumentWithVersionHandler(req, res) + : getDocumentHandler(req, res) } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/legacy-endpoints/getDocumentHistory.ts b/packages/app/pages/api/legacy-endpoints/getDocumentHistory.ts index 7dff25f3b..e03bc88fa 100644 --- a/packages/app/pages/api/legacy-endpoints/getDocumentHistory.ts +++ b/packages/app/pages/api/legacy-endpoints/getDocumentHistory.ts @@ -1,15 +1,13 @@ import type { NextApiRequest, NextApiResponse } from 'next' -import { apiError } from 'utils/errors' import getDocumentHistoryHandler from '../models/[modelName]/documents/[documentTitle]/history/' import { documentSchema } from './_schemas' +import { withApiErrorHandler } from 'lib/with-api-error-handler' -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - // replaces query params with params mapped to RESTful names (e.g. "model" -> "modelName", etc.) - req.query = documentSchema.parse(req.query) +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + // replaces query params with params mapped to RESTful names (e.g. "model" -> "modelName", etc.) + req.query = documentSchema.parse(req.query) - return getDocumentHistoryHandler(req, res) - } catch (err) { - return apiError(err, res) - } + return getDocumentHistoryHandler(req, res) } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/legacy-endpoints/getDocumentPublicationStatus.ts b/packages/app/pages/api/legacy-endpoints/getDocumentPublicationStatus.ts index dfe81cfc4..aa80e605b 100644 --- a/packages/app/pages/api/legacy-endpoints/getDocumentPublicationStatus.ts +++ b/packages/app/pages/api/legacy-endpoints/getDocumentPublicationStatus.ts @@ -1,15 +1,13 @@ import type { NextApiRequest, NextApiResponse } from 'next' -import { apiError } from 'utils/errors' import getDocumentPublicationStatusHandler from '../models/[modelName]/documents/[documentTitle]/publications/' import { documentSchema } from './_schemas' +import { withApiErrorHandler } from 'lib/with-api-error-handler' -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - // replaces query params with params mapped to RESTful names (e.g. "model" -> "modelName", etc.) - req.query = documentSchema.parse(req.query) +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + // replaces query params with params mapped to RESTful names (e.g. "model" -> "modelName", etc.) + req.query = documentSchema.parse(req.query) - return getDocumentPublicationStatusHandler(req, res) - } catch (err) { - return apiError(err, res) - } + return getDocumentPublicationStatusHandler(req, res) } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/legacy-endpoints/getModel.ts b/packages/app/pages/api/legacy-endpoints/getModel.ts index 32c97673d..ac3a28bd3 100644 --- a/packages/app/pages/api/legacy-endpoints/getModel.ts +++ b/packages/app/pages/api/legacy-endpoints/getModel.ts @@ -1,7 +1,7 @@ import type { NextApiRequest, NextApiResponse } from 'next' -import { apiError } from 'utils/errors' import getModelHandler from '../models/[modelName]/' import { z } from 'zod' +import { withApiErrorHandler } from 'lib/with-api-error-handler' const schema = z .object({ @@ -12,13 +12,11 @@ const schema = z modelName: name, })) -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - // replaces query params with params mapped to RESTful names (e.g. "model" -> "modelName", etc.) - req.query = schema.parse(req.query) +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + // replaces query params with params mapped to RESTful names (e.g. "model" -> "modelName", etc.) + req.query = schema.parse(req.query) - return getModelHandler(req, res) - } catch (err) { - return apiError(err, res) - } + return getModelHandler(req, res) } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/legacy-endpoints/listDocuments.ts b/packages/app/pages/api/legacy-endpoints/listDocuments.ts index 58491a0da..8a4be2211 100644 --- a/packages/app/pages/api/legacy-endpoints/listDocuments.ts +++ b/packages/app/pages/api/legacy-endpoints/listDocuments.ts @@ -1,15 +1,13 @@ import type { NextApiRequest, NextApiResponse } from 'next' -import { apiError } from 'utils/errors' import listDocumentsHandler from '../models/[modelName]/documents/' import { modelSchema } from './_schemas' +import { withApiErrorHandler } from 'lib/with-api-error-handler' -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - // replaces query params with params mapped to RESTful names (e.g. "model" -> "modelName", etc.) - req.query = modelSchema.parse(req.query) +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + // replaces query params with params mapped to RESTful names (e.g. "model" -> "modelName", etc.) + req.query = modelSchema.parse(req.query) - return listDocumentsHandler(req, res) - } catch (err) { - return apiError(err, res) - } + return listDocumentsHandler(req, res) } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/legacy-endpoints/listModels.ts b/packages/app/pages/api/legacy-endpoints/listModels.ts index 6b609e22b..68d06bfa5 100644 --- a/packages/app/pages/api/legacy-endpoints/listModels.ts +++ b/packages/app/pages/api/legacy-endpoints/listModels.ts @@ -1,6 +1,9 @@ import type { NextApiRequest, NextApiResponse } from 'next' import listModelsHandler from '../models/' +import { withApiErrorHandler } from 'lib/with-api-error-handler' -export default async function handler(req: NextApiRequest, res: NextApiResponse) { +const handler = async (req: NextApiRequest, res: NextApiResponse) => { return listModelsHandler(req, res) } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/legacy-endpoints/netrc-callback.ts b/packages/app/pages/api/legacy-endpoints/netrc-callback.ts index 112000fd1..d1ef15212 100644 --- a/packages/app/pages/api/legacy-endpoints/netrc-callback.ts +++ b/packages/app/pages/api/legacy-endpoints/netrc-callback.ts @@ -1,87 +1,79 @@ // pages/api/auth/custom-login.js import { basePath, EDLTokenSetParameters } from 'auth/providers/earthdata-login' import log from 'lib/log' +import { withApiErrorHandler } from 'lib/with-api-error-handler' import { NextApiRequest, NextApiResponse } from 'next' import { encode } from 'next-auth/jwt' -import { apiError, ErrorCode, HttpException } from 'utils/errors' +import createError from 'http-errors' +import assert from 'assert' -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - if (req.method !== 'GET') { - throw new HttpException(ErrorCode.MethodNotAllowed, 'Method not allowed') - } +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + assert(req.method === 'GET', new createError.MethodNotAllowed()) + assert( + req.query.code, + new createError.BadRequest('Missing `code` from Earthdata Login') + ) - if (!req.query.code) { - throw new HttpException( - ErrorCode.BadRequest, - 'Missing `code` from Earthdata Login' - ) - } + // we've successfully logged in, now we need to fetch a token from Earthdata Login + const tokenResult = await fetch(`${basePath}/oauth/token`, { + method: 'POST', + body: `grant_type=authorization_code&code=${ + req.query.code + }&redirect_uri=${encodeURIComponent( + `${process.env.HOST}/api/legacy-endpoints/netrc-callback` + )}`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: Buffer.from( + `${process.env.AUTH_CLIENT_ID}:${process.env.AUTH_CLIENT_SECRET}` + ).toString('base64'), + }, + }) - // we've successfully logged in, now we need to fetch a token from Earthdata Login - const tokenResult = await fetch(`${basePath}/oauth/token`, { - method: 'POST', - body: `grant_type=authorization_code&code=${ - req.query.code - }&redirect_uri=${encodeURIComponent( - `${process.env.HOST}/api/legacy-endpoints/netrc-callback` - )}`, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - Authorization: Buffer.from( - `${process.env.AUTH_CLIENT_ID}:${process.env.AUTH_CLIENT_SECRET}` - ).toString('base64'), - }, - }) + if (!tokenResult.ok) { + log.error( + `Failed to retrieve a token from Earthdata Login ${tokenResult.status}, ${tokenResult.statusText}` + ) - if (!tokenResult.ok) { - log.error( - `Failed to retrieve a token from Earthdata Login ${tokenResult.status}, ${tokenResult.statusText}` - ) - throw new HttpException( - ErrorCode.BadRequest, - 'Unable to retrieve token from Earthdata Login' - ) - } + throw new createError.BadRequest( + 'Unable to retrieve token from Earthdata Login' + ) + } - const tokenParameters: EDLTokenSetParameters = await tokenResult.json() + const tokenParameters: EDLTokenSetParameters = await tokenResult.json() - log.debug('Tokens returned from Earthdata Login: ', tokenParameters) + log.debug('Tokens returned from Earthdata Login: ', tokenParameters) - // now that we have our access token, we can request the logged in users information! - const userResult = await fetch(`${basePath}/oauth/userinfo`, { - headers: { - Authorization: `${tokenParameters.token_type} ${tokenParameters.access_token}`, - }, - }) + // now that we have our access token, we can request the logged in users information! + const userResult = await fetch(`${basePath}/oauth/userinfo`, { + headers: { + Authorization: `${tokenParameters.token_type} ${tokenParameters.access_token}`, + }, + }) - if (!userResult.ok) { - throw new HttpException( - ErrorCode.Unauthorized, - 'Failed to retrieve user information' - ) - } + if (!userResult.ok) { + throw new createError.Unauthorized('Failed to retrieve user information') + } - const userInfo = await userResult.json() - const maxAge = 30 * 24 * 60 * 60 // 30 days + const userInfo = await userResult.json() + const maxAge = 30 * 24 * 60 * 60 // 30 days - const nextAuthToken = await encode({ - token: { - email: userInfo.email_address, - name: `${userInfo.first_name} ${userInfo.last_name}`, - sub: userInfo.sub, - uid: userInfo.sub, - }, - secret: process.env.NEXTAUTH_SECRET!, - maxAge, - }) + const nextAuthToken = await encode({ + token: { + email: userInfo.email_address, + name: `${userInfo.first_name} ${userInfo.last_name}`, + sub: userInfo.sub, + uid: userInfo.sub, + }, + secret: process.env.NEXTAUTH_SECRET!, + maxAge, + }) - res.setHeader('Set-Cookie', [ - `next-auth.session-token=${nextAuthToken}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${maxAge}`, - ]) + res.setHeader('Set-Cookie', [ + `next-auth.session-token=${nextAuthToken}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${maxAge}`, + ]) - res.status(200).json(userInfo) - } catch (err) { - return apiError(err, res) - } + res.status(200).json(userInfo) } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/legacy-endpoints/netrc-login.ts b/packages/app/pages/api/legacy-endpoints/netrc-login.ts index 38b29b0cf..1a5c2c1d4 100644 --- a/packages/app/pages/api/legacy-endpoints/netrc-login.ts +++ b/packages/app/pages/api/legacy-endpoints/netrc-login.ts @@ -1,8 +1,9 @@ import { basePath } from 'auth/providers/earthdata-login' import log from 'lib/log' +import { withApiErrorHandler } from 'lib/with-api-error-handler' import type { NextApiRequest, NextApiResponse } from 'next' -export default async function handler(req: NextApiRequest, res: NextApiResponse) { +const handler = async (req: NextApiRequest, res: NextApiResponse) => { // construct our internal callback url to generate a NextAuth session after .netrc authentication to EDL const callbackUrl = `${process.env.HOST}/api/legacy-endpoints/netrc-callback` @@ -20,3 +21,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) // remember, on successful login, EDL will then redirect to our `callbackUrl` above res.redirect(302, earthdataAuthUrl) } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/legacy-endpoints/netrc-logout.ts b/packages/app/pages/api/legacy-endpoints/netrc-logout.ts index 9f48b474d..a2402aac4 100644 --- a/packages/app/pages/api/legacy-endpoints/netrc-logout.ts +++ b/packages/app/pages/api/legacy-endpoints/netrc-logout.ts @@ -1,9 +1,12 @@ +import { withApiErrorHandler } from 'lib/with-api-error-handler' import type { NextApiRequest, NextApiResponse } from 'next' -export default async function handler(req: NextApiRequest, res: NextApiResponse) { +const handler = async (req: NextApiRequest, res: NextApiResponse) => { res.setHeader('Set-Cookie', [ `next-auth.session-token=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0`, ]) res.status(204).end() } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/legacy-endpoints/netrc-me.ts b/packages/app/pages/api/legacy-endpoints/netrc-me.ts index 4f7c302d2..f563d4f6b 100644 --- a/packages/app/pages/api/legacy-endpoints/netrc-me.ts +++ b/packages/app/pages/api/legacy-endpoints/netrc-me.ts @@ -1,24 +1,14 @@ import type { NextApiRequest, NextApiResponse } from 'next' import { getLoggedInUser } from 'auth/user' import { respondAsJson } from 'utils/api' -import { apiError, ErrorCode, HttpException } from 'utils/errors' +import { withApiErrorHandler } from 'lib/with-api-error-handler' +import createError from 'http-errors' +import assert from 'assert' -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const user = await getLoggedInUser(req, res) +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + assert(req.method === 'GET', new createError.MethodNotAllowed()) - if (!user) { - return apiError( - new HttpException(ErrorCode.Unauthorized, 'Unauthorized'), - res - ) - } - - switch (req.method) { - case 'GET': { - return respondAsJson(user, req, res) - } - - default: - return res.status(405).end() - } + return respondAsJson(await getLoggedInUser(req, res), req, res) } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/legacy-endpoints/putDocument.ts b/packages/app/pages/api/legacy-endpoints/putDocument.ts index 5283a34d0..bef7ae394 100644 --- a/packages/app/pages/api/legacy-endpoints/putDocument.ts +++ b/packages/app/pages/api/legacy-endpoints/putDocument.ts @@ -1,60 +1,55 @@ import type { NextApiRequest, NextApiResponse } from 'next' import putDocumentHandler from '../models/[modelName]/documents/' -import { apiError, ErrorCode, HttpException } from 'utils/errors' +import { withApiErrorHandler } from 'lib/with-api-error-handler' +import createError from 'http-errors' +import assert from 'assert' // our new API supports uploading a document as JSON // however the legacy API only supported file based uploads, which are difficult to use // we'll need to parse the file upload here and pass the resulting JSON on to the new RESTful API -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - if (req.method !== 'POST') { - throw new HttpException( - ErrorCode.MethodNotAllowed, - `Method ${req.method} Not Allowed` - ) - } +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + assert(req.method === 'POST', new createError.MethodNotAllowed()) - //! Step 1: Extract the boundary from the Content-Type header - const contentType = req.headers['content-type'] + //! Step 1: Extract the boundary from the Content-Type header + const contentType = req.headers['content-type'] - if (!contentType?.startsWith('multipart/form-data')) { - throw new HttpException(ErrorCode.BadRequest, 'Invalid content type') - } + if (!contentType?.startsWith('multipart/form-data')) { + throw new createError.BadRequest('Invalid content type') + } - const boundary = contentType.split('boundary=')[1] + const boundary = contentType.split('boundary=')[1] - if (!boundary) { - throw new HttpException(ErrorCode.BadRequest, 'Boundary not found') - } + if (!boundary) { + throw new createError.BadRequest('Boundary not found') + } - //! Step 2: Split the raw data into parts - const parts = req.body.toString().split(`--${boundary}`) - - //! Step 3: Parse each part to find the file - for (const part of parts) { - if ( - part.includes('Content-Disposition: form-data;') && - part.includes('filename="') - ) { - // Extract the JSON content - const jsonStart = part.indexOf('\r\n\r\n') + 4 // Find the start of the file content - const jsonEnd = part.lastIndexOf('\r\n') - const fileContent = part.slice(jsonStart, jsonEnd).trim() - - //! Step 4: retrieve the model name from the JSON content and add the file content as the new body - const document = JSON.parse(fileContent) - req.query.modelName = encodeURIComponent(document['x-meditor'].model) - req.body = fileContent - - //! Step 5: call the new REST endpoint handler with the JSON content as the body - req.body = fileContent - return putDocumentHandler(req, res) - } + //! Step 2: Split the raw data into parts + const parts = req.body.toString().split(`--${boundary}`) + + //! Step 3: Parse each part to find the file + for (const part of parts) { + if ( + part.includes('Content-Disposition: form-data;') && + part.includes('filename="') + ) { + // Extract the JSON content + const jsonStart = part.indexOf('\r\n\r\n') + 4 // Find the start of the file content + const jsonEnd = part.lastIndexOf('\r\n') + const fileContent = part.slice(jsonStart, jsonEnd).trim() + + //! Step 4: retrieve the model name from the JSON content and add the file content as the new body + const document = JSON.parse(fileContent) + req.query.modelName = encodeURIComponent(document['x-meditor'].model) + req.body = fileContent + + //! Step 5: call the new REST endpoint handler with the JSON content as the body + req.body = fileContent + return putDocumentHandler(req, res) } - - // if we get here, no file was found in the body - throw new HttpException(ErrorCode.BadRequest, 'No file found in form-data') - } catch (err) { - return apiError(err, res) } + + // if we get here, no file was found in the body + throw new createError.BadRequest('No file found in form-data') } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/[documentVersion]/index.ts b/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/[documentVersion]/index.ts index 3034bc9e3..5fd9f866b 100644 --- a/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/[documentVersion]/index.ts +++ b/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/[documentVersion]/index.ts @@ -1,32 +1,31 @@ -import type { NextApiRequest, NextApiResponse } from 'next' -import { getLoggedInUser } from '../../../../../../../auth/user' +import assert from 'assert' +import createError from 'http-errors' import { getDocument } from '../../../../../../../documents/service' +import { getLoggedInUser } from '../../../../../../../auth/user' import { respondAsJson } from '../../../../../../../utils/api' -import { apiError } from '../../../../../../../utils/errors' +import { withApiErrorHandler } from 'lib/with-api-error-handler' +import type { NextApiRequest, NextApiResponse } from 'next' + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + assert(req.method === 'GET', new createError.MethodNotAllowed()) -export default async function handler(req: NextApiRequest, res: NextApiResponse) { const documentTitle = decodeURIComponent(req.query.documentTitle.toString()) const documentVersion = decodeURIComponent(req.query.documentVersion.toString()) const modelName = decodeURIComponent(req.query.modelName.toString()) const user = await getLoggedInUser(req, res) - switch (req.method) { - case 'GET': { - const [error, document] = await getDocument( - documentTitle, - modelName, - user, - documentVersion - ) + const [error, document] = await getDocument( + documentTitle, + modelName, + user, + documentVersion + ) - if (error) { - return apiError(error, res) - } - - return respondAsJson(document, req, res) - } - - default: - return res.status(405).end() + if (error) { + throw error } + + return respondAsJson(document, req, res) } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/change-document-state.ts b/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/change-document-state.ts index 967cb0f66..826091285 100644 --- a/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/change-document-state.ts +++ b/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/change-document-state.ts @@ -2,22 +2,22 @@ import type { NextApiRequest, NextApiResponse } from 'next' import { getLoggedInUser } from '../../../../../../auth/user' import { changeDocumentState } from '../../../../../../documents/service' import { respondAsJson } from '../../../../../../utils/api' -import { apiError, ErrorCode, HttpException } from '../../../../../../utils/errors' +import assert from 'assert' +import { withApiErrorHandler } from 'lib/with-api-error-handler' +import createError from 'http-errors' + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + assert( + req.method === 'PUT' || req.method === 'POST', + new createError.MethodNotAllowed() + ) -export default async function handler(req: NextApiRequest, res: NextApiResponse) { const documentTitle = decodeURIComponent(req.query.documentTitle.toString()) const modelName = decodeURIComponent(req.query.modelName.toString()) const newState = decodeURIComponent(req.query.state?.toString()) const user = await getLoggedInUser(req, res) - if (req.method !== 'PUT' && req.method !== 'POST') { - return apiError( - new HttpException(ErrorCode.MethodNotAllowed, 'Method not allowed'), - res - ) - } - const shouldUpdateDocument = req.method === 'PUT' && req.body && Object.keys(req.body).length > 0 @@ -37,8 +37,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ) if (error) { - return apiError(error, res) + throw error } return respondAsJson(document, req, res) } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/clone-document.ts b/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/clone-document.ts index 9fe3c1a3d..3c188089e 100644 --- a/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/clone-document.ts +++ b/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/clone-document.ts @@ -2,35 +2,31 @@ import type { NextApiRequest, NextApiResponse } from 'next' import { getLoggedInUser } from '../../../../../../auth/user' import { cloneDocument } from '../../../../../../documents/service' import { respondAsJson } from '../../../../../../utils/api' -import { apiError, ErrorCode, HttpException } from '../../../../../../utils/errors' +import assert from 'assert' +import { withApiErrorHandler } from 'lib/with-api-error-handler' +import createError from 'http-errors' + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + assert(req.method === 'POST', new createError.MethodNotAllowed()) -export default async function handler(req: NextApiRequest, res: NextApiResponse) { const documentTitle = decodeURIComponent(req.query.documentTitle.toString()) const modelName = decodeURIComponent(req.query.modelName.toString()) const newTitle = decodeURIComponent(req.query.newTitle.toString()) const user = await getLoggedInUser(req, res) - switch (req.method) { - case 'POST': { - const [error, document] = await cloneDocument( - documentTitle, - newTitle, - modelName, - user - ) - - if (error) { - return apiError(error, res) - } - - // todo: discuss this vs createDocument's 201 w/ location header; api-safe? - return respondAsJson(document, req, res) - } + const [error, document] = await cloneDocument( + documentTitle, + newTitle, + modelName, + user + ) - default: - return apiError( - new HttpException(ErrorCode.MethodNotAllowed, 'Method not allowed'), - res - ) + if (error) { + throw error } + + // todo: discuss this vs createDocument's 201 w/ location header; api-safe? + return respondAsJson(document, req, res) } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/comments/[commentId]/index.ts b/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/comments/[commentId]/index.ts index 2e3c99cc7..611179113 100644 --- a/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/comments/[commentId]/index.ts +++ b/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/comments/[commentId]/index.ts @@ -1,29 +1,27 @@ -import type { NextApiRequest, NextApiResponse } from 'next' +import assert from 'assert' +import createError from 'http-errors' import { getLoggedInUser } from '../../../../../../../../auth/user' +import { respondAsJson } from '../../../../../../../../utils/api' +import { withApiErrorHandler } from 'lib/with-api-error-handler' +import type { NextApiRequest, NextApiResponse } from 'next' import { getCommentForDocument, updateCommentAsUser, } from '../../../../../../../../comments/service' -import { respondAsJson } from '../../../../../../../../utils/api' import { apiError, ErrorCode, HttpException, } from '../../../../../../../../utils/errors' -export default async function handler(req: NextApiRequest, res: NextApiResponse) { +const handler = async (req: NextApiRequest, res: NextApiResponse) => { const commentId = decodeURIComponent(req.query.commentId.toString()) const documentTitle = decodeURIComponent(req.query.documentTitle.toString()) const modelName = decodeURIComponent(req.query.modelName.toString()) const user = await getLoggedInUser(req, res) // user should be logged in for any comments related activity - if (!user) { - return apiError( - new HttpException(ErrorCode.Unauthorized, 'Unauthorized'), - res - ) - } + assert(user, new createError.Unauthorized()) switch (req.method) { case 'GET': { @@ -34,7 +32,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ) if (error) { - return apiError(error, res) + throw error } return respondAsJson(comment, req, res) @@ -54,12 +52,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (error) { // if we see an error here, it's most likely due to a database issue. Without exposing the error itself, the best we can do // is ask the user to try again - return apiError(error, res) + throw error } return respondAsJson(updatedComment, req, res) default: - return res.status(405).end() + throw new createError.MethodNotAllowed() } } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/comments/index.ts b/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/comments/index.ts index 528862246..3d10d0b59 100644 --- a/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/comments/index.ts +++ b/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/comments/index.ts @@ -1,22 +1,18 @@ -import type { NextApiRequest, NextApiResponse } from 'next' +import assert from 'assert' +import createError from 'http-errors' import { getLoggedInUser } from '../../../../../../../auth/user' +import { respondAsJson } from '../../../../../../../utils/api' +import { withApiErrorHandler } from 'lib/with-api-error-handler' +import type { NextApiRequest, NextApiResponse } from 'next' import { createCommentAsUser, getCommentsForDocument, } from '../../../../../../../comments/service' -import { respondAsJson } from '../../../../../../../utils/api' -import { apiError, ErrorCode, HttpException } from '../../../../../../../utils/errors' -export default async function handler(req: NextApiRequest, res: NextApiResponse) { +const handler = async (req: NextApiRequest, res: NextApiResponse) => { const user = await getLoggedInUser(req, res) - // user should be logged in for any comments related activity - if (!user) { - return apiError( - new HttpException(ErrorCode.Unauthorized, 'Unauthorized'), - res - ) - } + assert(user, new createError.Unauthorized()) const documentTitle = decodeURIComponent(req.query.documentTitle.toString()) const modelName = decodeURIComponent(req.query.modelName.toString()) @@ -29,7 +25,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ) if (error) { - return apiError(error, res) + throw error } return respondAsJson(comments, req, res) @@ -46,13 +42,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ) if (error) { - return apiError(error, res) + throw error } return respondAsJson(newComment, req, res) } default: - return res.status(405).end() + throw new createError.MethodNotAllowed() } } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/history/[revisionId]/index.ts b/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/history/[revisionId]/index.ts index 58fd6dbe2..57cc27ba1 100644 --- a/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/history/[revisionId]/index.ts +++ b/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/history/[revisionId]/index.ts @@ -1,29 +1,28 @@ -import type { NextApiRequest, NextApiResponse } from 'next' +import assert from 'assert' +import createError from 'http-errors' import { getDocumentHistoryByVersion } from '../../../../../../../../documents/service' import { respondAsJson } from '../../../../../../../../utils/api' -import { apiError } from '../../../../../../../../utils/errors' +import { withApiErrorHandler } from 'lib/with-api-error-handler' +import type { NextApiRequest, NextApiResponse } from 'next' + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + assert(req.method === 'GET', new createError.MethodNotAllowed()) -export default async function handler(req: NextApiRequest, res: NextApiResponse) { const documentTitle = decodeURIComponent(req.query.documentTitle.toString()) const modelName = decodeURIComponent(req.query.modelName.toString()) const revisionId = decodeURIComponent(req.query.revisionId.toString()) - switch (req.method) { - case 'GET': { - const [error, history] = await getDocumentHistoryByVersion( - revisionId, - documentTitle, - modelName - ) - - if (error) { - return apiError(error, res) - } + const [error, history] = await getDocumentHistoryByVersion( + revisionId, + documentTitle, + modelName + ) - return respondAsJson(history, req, res) - } - - default: - return res.status(405).end() + if (error) { + throw error } + + return respondAsJson(history, req, res) } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/history/index.ts b/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/history/index.ts index d24e0b441..d6cbdc0f2 100644 --- a/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/history/index.ts +++ b/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/history/index.ts @@ -1,27 +1,23 @@ -import type { NextApiRequest, NextApiResponse } from 'next' +import assert from 'assert' +import createError from 'http-errors' import { getDocumentHistory } from '../../../../../../../documents/service' import { respondAsJson } from '../../../../../../../utils/api' -import { apiError } from '../../../../../../../utils/errors' +import { withApiErrorHandler } from 'lib/with-api-error-handler' +import type { NextApiRequest, NextApiResponse } from 'next' + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + assert(req.method === 'GET', new createError.MethodNotAllowed()) -export default async function handler(req: NextApiRequest, res: NextApiResponse) { const documentTitle = decodeURIComponent(req.query.documentTitle.toString()) const modelName = decodeURIComponent(req.query.modelName.toString()) - switch (req.method) { - case 'GET': { - const [error, history] = await getDocumentHistory( - documentTitle, - modelName - ) - - if (error) { - return apiError(error, res) - } + const [error, history] = await getDocumentHistory(documentTitle, modelName) - return respondAsJson(history, req, res) - } - - default: - return res.status(405).end() + if (error) { + throw error } + + return respondAsJson(history, req, res) } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/index.ts b/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/index.ts index 780d26ffc..a14552f2b 100644 --- a/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/index.ts +++ b/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/index.ts @@ -3,39 +3,29 @@ import { getLoggedInUser } from 'auth/user' import { getDocument } from 'documents/service' import { userCanAccessModel } from 'models/service' import { respondAsJson } from 'utils/api' -import { apiError, ErrorCode, HttpException } from 'utils/errors' +import assert from 'assert' +import { withApiErrorHandler } from 'lib/with-api-error-handler' +import createError from 'http-errors' + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + assert(req.method === 'GET', new createError.MethodNotAllowed()) -export default async function handler(req: NextApiRequest, res: NextApiResponse) { const documentTitle = decodeURIComponent(req.query.documentTitle.toString()) const modelName = decodeURIComponent(req.query.modelName.toString()) const user = await getLoggedInUser(req, res) - if (!userCanAccessModel(modelName, user)) { - return apiError( - new HttpException( - ErrorCode.ForbiddenError, - 'User does not have access to the requested model' - ), - res - ) - } - - switch (req.method) { - case 'GET': { - const [error, document] = await getDocument( - documentTitle, - modelName, - user - ) + assert( + await userCanAccessModel(user, modelName), + new createError.Forbidden('User does not have access to the requested model') + ) - if (error) { - return apiError(error, res) - } + const [error, document] = await getDocument(documentTitle, modelName, user) - return respondAsJson(document, req, res) - } - - default: - return res.status(405).end() + if (error) { + throw error } + + return respondAsJson(document, req, res) } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/publications/index.ts b/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/publications/index.ts index 29cc133e9..e094f47c7 100644 --- a/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/publications/index.ts +++ b/packages/app/pages/api/models/[modelName]/documents/[documentTitle]/publications/index.ts @@ -1,27 +1,26 @@ -import type { NextApiRequest, NextApiResponse } from 'next' +import assert from 'assert' +import createError from 'http-errors' import { getDocumentPublications } from '../../../../../../../documents/service' import { respondAsJson } from '../../../../../../../utils/api' -import { apiError } from '../../../../../../../utils/errors' +import { withApiErrorHandler } from 'lib/with-api-error-handler' +import type { NextApiRequest, NextApiResponse } from 'next' + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + assert(req.method === 'GET', new createError.MethodNotAllowed()) -export default async function handler(req: NextApiRequest, res: NextApiResponse) { const documentTitle = decodeURIComponent(req.query.documentTitle.toString()) const modelName = decodeURIComponent(req.query.modelName.toString()) - switch (req.method) { - case 'GET': { - const [error, publications] = await getDocumentPublications( - documentTitle, - modelName - ) - - if (error) { - return apiError(error, res) - } + const [error, publications] = await getDocumentPublications( + documentTitle, + modelName + ) - return respondAsJson(publications, req, res) - } - - default: - return res.status(405).end() + if (error) { + throw error } + + return respondAsJson(publications, req, res) } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/models/[modelName]/documents/bulk/change-document-state.ts b/packages/app/pages/api/models/[modelName]/documents/bulk/change-document-state.ts index 8b8a44b9a..a1142c811 100644 --- a/packages/app/pages/api/models/[modelName]/documents/bulk/change-document-state.ts +++ b/packages/app/pages/api/models/[modelName]/documents/bulk/change-document-state.ts @@ -1,30 +1,21 @@ -import type { NextApiRequest, NextApiResponse } from 'next' -import { getLoggedInUser } from 'auth/user' +import assert from 'assert' +import createError from 'http-errors' import { bulkChangeDocumentState } from 'documents/service' -import { respondAsJson } from 'utils/api' -import { - apiError, - ErrorCode, - formatZodError, - HttpException, - parseZodAsErrorData, -} from 'utils/errors' import { bulkDocumentHeadersSchema } from 'documents/schema' +import { formatZodError, withApiErrorHandler } from 'lib/with-api-error-handler' +import { getLoggedInUser } from 'auth/user' +import { parseZodAsErrorData } from 'utils/errors' +import { respondAsJson } from 'utils/api' +import type { NextApiRequest, NextApiResponse } from 'next' import type { ZodError } from 'zod' -export default async function handler(req: NextApiRequest, res: NextApiResponse) { +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + assert(req.method === 'POST', new createError.MethodNotAllowed()) + const modelName = decodeURIComponent(req.query.modelName.toString()) const newState = decodeURIComponent(req.query.state?.toString()) - const user = await getLoggedInUser(req, res) - if (req.method !== 'POST') { - return apiError( - new HttpException(ErrorCode.MethodNotAllowed, 'Method not allowed'), - res - ) - } - //* we enforce requiring the user to provide explicit identifiers for the documents to patch (we don't support all) //* the standard is to use an "If-Match" header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Match const [headersError, headers] = parseZodAsErrorData( @@ -33,10 +24,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ) if (headersError) { - return apiError( - formatZodError(headersError as ZodError, '`If-Match` header: '), // format the ZodError so we can use a custom error message prefix - res - ) + throw formatZodError(headersError as ZodError, '`If-Match` header: ') // format the ZodError so we can use a custom error message prefix } //* parse the document titles from 'If-Match' @@ -57,8 +45,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ) if (error) { - return apiError(error, res) + throw error } return respondAsJson(result, req, res) } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/models/[modelName]/documents/bulk/index.ts b/packages/app/pages/api/models/[modelName]/documents/bulk/index.ts index feb777a54..bceef2f9c 100644 --- a/packages/app/pages/api/models/[modelName]/documents/bulk/index.ts +++ b/packages/app/pages/api/models/[modelName]/documents/bulk/index.ts @@ -8,82 +8,61 @@ import type { JSONPatchDocument } from 'immutable-json-patch' import { userCanAccessModel } from 'models/service' import type { NextApiRequest, NextApiResponse } from 'next' import { respondAsJson } from 'utils/api' -import { - apiError, - ErrorCode, - formatZodError, - HttpException, - parseZodAsErrorData, -} from 'utils/errors' +import { parseZodAsErrorData } from 'utils/errors' import type { ZodError } from 'zod' +import assert from 'assert' +import { formatZodError, withApiErrorHandler } from 'lib/with-api-error-handler' +import createError from 'http-errors' + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + assert(req.method === 'PATCH', new createError.MethodNotAllowed()) -export default async function handler(req: NextApiRequest, res: NextApiResponse) { const modelName = decodeURIComponent(req.query.modelName.toString()) const user = await getLoggedInUser(req, res) - if (!userCanAccessModel(modelName, user)) { - return apiError( - new HttpException( - ErrorCode.ForbiddenError, - 'User does not have access to the requested model' - ), - res - ) - } - - switch (req.method) { - case 'PATCH': { - if (!user) { - return apiError( - new HttpException(ErrorCode.Unauthorized, 'Unauthorized'), - res - ) - } - - //* we enforce requiring the user to provide explicit identifiers for the documents to patch (we don't support all/*) - //* the standard is to use an "If-Match" header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Match - const [headersError, headers] = parseZodAsErrorData( - bulkDocumentHeadersSchema, - req.headers - ) + assert( + await userCanAccessModel(user, modelName), + new createError.Forbidden('User does not have access to the requested model') + ) - if (headersError) { - return apiError( - formatZodError(headersError as ZodError, '`If-Match` header: '), // format the ZodError so we can use a custom error message prefix - res - ) - } + //* we enforce requiring the user to provide explicit identifiers for the documents to patch (we don't support all/*) + //* the standard is to use an "If-Match" header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Match + const [headersError, headers] = parseZodAsErrorData( + bulkDocumentHeadersSchema, + req.headers + ) - //* parse the document titles from 'If-Match' - const documentTitles = headers['if-match'] - .split(',') - .map(title => title.trim().replace(/^"/, '').replace(/"$/, '')) - - //* we'll also parse the list of operations the user requested, this will ensure they match the right format for the JSON patch spec - const [parsingError, operations] = parseZodAsErrorData( - patchDocumentsInputSchema, - req.body - ) + if (headersError) { + throw formatZodError(headersError as ZodError, '`If-Match` header: ') // format the ZodError so we can use a custom error message prefix + } - if (parsingError) { - return apiError(parsingError, res) - } + //* parse the document titles from 'If-Match' + const documentTitles = headers['if-match'] + .split(',') + .map(title => title.trim().replace(/^"/, '').replace(/"$/, '')) - const [error, result] = await bulkPatchDocuments( - documentTitles, - modelName, - user, - operations - ) + //* we'll also parse the list of operations the user requested, this will ensure they match the right format for the JSON patch spec + const [parsingError, operations] = parseZodAsErrorData( + patchDocumentsInputSchema, + req.body + ) - if (error) { - return apiError(error, res) - } + if (parsingError) { + throw parsingError + } - return respondAsJson(result, req, res) - } + const [error, result] = await bulkPatchDocuments( + documentTitles, + modelName, + user, + operations + ) - default: - return res.status(405).end() + if (error) { + throw error } + + return respondAsJson(result, req, res) } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/models/[modelName]/documents/index.ts b/packages/app/pages/api/models/[modelName]/documents/index.ts index fcdf5eb4f..4dea2356f 100755 --- a/packages/app/pages/api/models/[modelName]/documents/index.ts +++ b/packages/app/pages/api/models/[modelName]/documents/index.ts @@ -3,22 +3,19 @@ import { createDocument, getDocumentsForModel } from 'documents/service' import { userCanAccessModel } from 'models/service' import type { NextApiRequest, NextApiResponse } from 'next' import { respondAsJson } from 'utils/api' -import { apiError, ErrorCode, HttpException } from 'utils/errors' import { safeParseJSON } from 'utils/json' +import assert from 'assert' +import { withApiErrorHandler } from 'lib/with-api-error-handler' +import createError from 'http-errors' -export default async function handler(req: NextApiRequest, res: NextApiResponse) { +const handler = async (req: NextApiRequest, res: NextApiResponse) => { const modelName = decodeURIComponent(req.query.modelName.toString()) const user = await getLoggedInUser(req, res) - if (!userCanAccessModel(modelName, user)) { - return apiError( - new HttpException( - ErrorCode.ForbiddenError, - 'User does not have access to the requested model' - ), - res - ) - } + assert( + await userCanAccessModel(user, modelName), + new createError.Forbidden('User does not have access to the requested model') + ) switch (req.method) { case 'GET': { @@ -31,24 +28,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }) if (error) { - return apiError(error, res) + throw error } return respondAsJson(documents, req, res) } case 'POST': { - if (!user) { - return apiError( - new HttpException(ErrorCode.Unauthorized, 'Unauthorized'), - res - ) - } - const [parsingError, parsedDocument] = safeParseJSON(req.body) if (parsingError) { - return apiError(parsingError, res) + throw parsingError } const [documentError, data] = await createDocument( @@ -59,7 +49,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ) if (documentError) { - return apiError(documentError, res) + throw documentError } const { _id, ...apiSafeDocument } = data.insertedDocument @@ -72,6 +62,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } default: - return res.status(405).end() + throw new createError.MethodNotAllowed() } } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/models/[modelName]/index.ts b/packages/app/pages/api/models/[modelName]/index.ts index ac9ca5581..e1eb0033e 100644 --- a/packages/app/pages/api/models/[modelName]/index.ts +++ b/packages/app/pages/api/models/[modelName]/index.ts @@ -1,41 +1,35 @@ -import type { NextApiRequest, NextApiResponse } from 'next' +import assert from 'assert' +import createError from 'http-errors' import { getLoggedInUser } from 'auth/user' import { getModel, userCanAccessModel } from 'models/service' import { respondAsJson } from 'utils/api' -import { apiError, ErrorCode, HttpException } from 'utils/errors' +import { withApiErrorHandler } from 'lib/with-api-error-handler' +import type { NextApiRequest, NextApiResponse } from 'next' + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + assert(req.method === 'GET', new createError.MethodNotAllowed()) -export default async function handler(req: NextApiRequest, res: NextApiResponse) { const modelName = decodeURIComponent(req.query.modelName.toString()) const disableMacros = 'disableMacros' in req.query const user = await getLoggedInUser(req, res) - if (!userCanAccessModel(modelName, user)) { - return apiError( - new HttpException( - ErrorCode.ForbiddenError, - 'User does not have access to the requested model' - ), - res - ) - } - - switch (req.method) { - case 'GET': { - const [error, model] = await getModel(modelName, { - //* Do not expose DB ID to API. - includeId: false, - //* Allow boolean search param to optionally disable template macros. Defaults to running macros. - populateMacroTemplates: !disableMacros, - }) - - if (error) { - return apiError(error, res) - } + assert( + await userCanAccessModel(user, modelName), + new createError.Forbidden('User does not have access to the requested model') + ) - return respondAsJson(model, req, res) - } + const [error, model] = await getModel(modelName, { + //* Do not expose DB ID to API. + includeId: false, + //* Allow boolean search param to optionally disable template macros. Defaults to running macros. + populateMacroTemplates: !disableMacros, + }) - default: - return res.status(405).end() + if (error) { + throw error } + + return respondAsJson(model, req, res) } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/models/[modelName]/schema.ts b/packages/app/pages/api/models/[modelName]/schema.ts index bec50bf4d..7f6f145c8 100644 --- a/packages/app/pages/api/models/[modelName]/schema.ts +++ b/packages/app/pages/api/models/[modelName]/schema.ts @@ -2,37 +2,31 @@ import type { NextApiRequest, NextApiResponse } from 'next' import { getLoggedInUser } from 'auth/user' import { getModel, userCanAccessModel } from 'models/service' import { respondAsJson } from 'utils/api' -import { apiError, ErrorCode, HttpException } from 'utils/errors' +import { withApiErrorHandler } from 'lib/with-api-error-handler' +import assert from 'assert' +import createError from 'http-errors' + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + assert(req.method === 'GET', new createError.MethodNotAllowed()) -export default async function handler(req: NextApiRequest, res: NextApiResponse) { const modelName = decodeURIComponent(req.query.modelName.toString()) const user = await getLoggedInUser(req, res) - if (!userCanAccessModel(modelName, user)) { - return apiError( - new HttpException( - ErrorCode.ForbiddenError, - 'User does not have access to the requested model' - ), - res - ) - } - - switch (req.method) { - case 'GET': { - const [error, model] = await getModel(modelName, { - includeId: false, - populateMacroTemplates: true, - }) + assert( + await userCanAccessModel(user, modelName), + new createError.Forbidden('User does not have access to the requested model') + ) - if (error) { - return apiError(error, res) - } + const [error, model] = await getModel(modelName, { + includeId: false, + populateMacroTemplates: true, + }) - return respondAsJson(JSON.parse(model.schema), req, res) - } - - default: - return res.status(405).end() + if (error) { + throw error } + + return respondAsJson(JSON.parse(model.schema), req, res) } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/models/[modelName]/search/index.ts b/packages/app/pages/api/models/[modelName]/search/index.ts index 2184080b5..dbce97279 100644 --- a/packages/app/pages/api/models/[modelName]/search/index.ts +++ b/packages/app/pages/api/models/[modelName]/search/index.ts @@ -4,54 +4,48 @@ import type { NextApiRequest, NextApiResponse } from 'next' import { searchInputApiSchema } from 'search/schema' import { search } from 'search/service' import { respondAs } from 'utils/api' -import { ErrorCode, HttpException, apiError, parseZodAsErrorData } from 'utils/errors' +import { parseZodAsErrorData } from 'utils/errors' import type { z } from 'zod' +import assert from 'assert' +import { withApiErrorHandler } from 'lib/with-api-error-handler' +import createError from 'http-errors' + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + assert(req.method === 'GET', new createError.MethodNotAllowed()) -//* beef up apiError to format and handle ZodErrors; create a util to turn safeParse into ErrorData -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const user = await getLoggedInUser(req, res) const [parsingError, parsedData] = parseZodAsErrorData< z.infer >(searchInputApiSchema, req.query) if (parsingError) { - return apiError(parsingError, res) + throw parsingError } const { query, format, modelName, resultsPerPage, pageNumber } = parsedData + const user = await getLoggedInUser(req, res) - if (!userCanAccessModel(modelName.toString(), user)) { - return apiError( - new HttpException( - ErrorCode.ForbiddenError, - 'User does not have access to the requested model.' - ), - res - ) - } + assert( + await userCanAccessModel(user, modelName.toString()), + new createError.Forbidden('User does not have access to the requested model') + ) - switch (req.method) { - case 'GET': { - const [error, searchResults] = await search( - modelName, - query, - resultsPerPage, - pageNumber - ) - - if (error) { - return apiError(error, res) - } - - return await respondAs(searchResults, req, res, { - format, - //* The union doesn't show it, but we have a transform on the Zod schema to uppercase this property. - //* Sending the metadata property to the CSV parser changes the column headers, so just send the results. - payloadPath: format === 'JSON' ? '' : 'results', - }) - } - - default: - return res.status(405).end() + const [error, searchResults] = await search( + modelName, + query, + resultsPerPage, + pageNumber + ) + + if (error) { + throw error } + + return await respondAs(searchResults, req, res, { + format, + //* The union doesn't show it, but we have a transform on the Zod schema to uppercase this property. + //* Sending the metadata property to the CSV parser changes the column headers, so just send the results. + payloadPath: format === 'JSON' ? '' : 'results', + }) } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/models/[modelName]/validate/index.ts b/packages/app/pages/api/models/[modelName]/validate/index.ts index 8184cd041..84d2e4f1c 100644 --- a/packages/app/pages/api/models/[modelName]/validate/index.ts +++ b/packages/app/pages/api/models/[modelName]/validate/index.ts @@ -3,47 +3,40 @@ import { strictValidateDocument } from 'documents/service' import { userCanAccessModel } from 'models/service' import type { NextApiRequest, NextApiResponse } from 'next' import { respondAsJson } from 'utils/api' -import { apiError, ErrorCode, HttpException } from 'utils/errors' import { safeParseJSON } from 'utils/json' +import assert from 'assert' +import { withApiErrorHandler } from 'lib/with-api-error-handler' +import createError from 'http-errors' + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + assert(req.method === 'POST', new createError.MethodNotAllowed()) -export default async function handler(req: NextApiRequest, res: NextApiResponse) { const modelName = decodeURIComponent(req.query.modelName.toString()) const user = await getLoggedInUser(req, res) - if (!userCanAccessModel(modelName, user)) { - return apiError( - new HttpException( - ErrorCode.ForbiddenError, - 'User does not have access to the requested model' - ), - res - ) - } - - switch (req.method) { - //* Unlike most POST endpoints, this allows unauthenticated access. - case 'POST': { - const [parsingError, parsedDocument] = safeParseJSON(req.body) - - if (parsingError) { - return apiError(parsingError, res) - } + assert( + await userCanAccessModel(user, modelName), + new createError.Forbidden('User does not have access to the requested model') + ) - const [validationError, validDocument] = await strictValidateDocument( - parsedDocument, - modelName - ) + const [parsingError, parsedDocument] = safeParseJSON(req.body) - if (validationError) { - return apiError(validationError, res) - } + if (parsingError) { + throw parsingError + } - return respondAsJson(validDocument, req, res, { - httpStatusCode: 200, - }) - } + const [validationError, validDocument] = await strictValidateDocument( + parsedDocument, + modelName + ) - default: - return res.status(405).end() + if (validationError) { + throw validationError } + + return respondAsJson(validDocument, req, res, { + httpStatusCode: 200, + }) } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/models/index.ts b/packages/app/pages/api/models/index.ts index 95c0f0fd5..c23ab7c28 100644 --- a/packages/app/pages/api/models/index.ts +++ b/packages/app/pages/api/models/index.ts @@ -1,21 +1,20 @@ import type { NextApiRequest, NextApiResponse } from 'next' import { getModels } from '../../../models/service' import { respondAsJson } from '../../../utils/api' -import { apiError } from '../../../utils/errors' +import assert from 'assert' +import createError from 'http-errors' +import { withApiErrorHandler } from 'lib/with-api-error-handler' -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - switch (req.method) { - case 'GET': { - const [error, models] = await getModels() +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + assert(req.method === 'GET', new createError.MethodNotAllowed()) - if (error) { - return apiError(error, res) - } + const [error, models] = await getModels() - return respondAsJson(models, req, res) - } - - default: - return res.status(405).end() + if (error) { + throw error } + + return respondAsJson(models, req, res) } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/api/validate/url-resolves.ts b/packages/app/pages/api/validate/url-resolves.ts index 22a2e6605..978bdee6e 100644 --- a/packages/app/pages/api/validate/url-resolves.ts +++ b/packages/app/pages/api/validate/url-resolves.ts @@ -1,69 +1,61 @@ -import type { NextApiRequest, NextApiResponse } from 'next' +import assert from 'assert' +import createError from 'http-errors' +import log from 'lib/log' import { getLoggedInUser } from '../../../auth/user' -import { apiError, ErrorCode, HttpException } from '../../../utils/errors' +import { withApiErrorHandler } from 'lib/with-api-error-handler' +import type { NextApiRequest, NextApiResponse } from 'next' + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + assert(req.method === 'POST', new createError.MethodNotAllowed()) -export default async function handler(req: NextApiRequest, res: NextApiResponse) { // this doesn't really need to be a secure endpoint BUT this is just an extra step to ensure no anonymous users are // hitting the link checker API const user = await getLoggedInUser(req, res) + assert(user, new createError.Unauthorized()) - if (!user) { - return apiError( - new HttpException(ErrorCode.Unauthorized, 'Unauthorized'), - res - ) - } - - switch (req.method) { - case 'POST': { - let isValid - - try { - const url = req.body.url - - let regex = new RegExp( - /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi - ) + let isValid - if (!url || !url.match(regex)) { - throw new Error('Invalid URL') - } + try { + const url = req.body.url - // try a HEAD request first - // this is the fastest way to check as we don't get the whole page back and most servers support this - let response = await fetch(url, { - method: 'HEAD', - }) - - if (response.status >= 400 && response.status !== 404) { - // servers SHOULD respond with a 404 if the page doesn't exist - // this one didn't, so let's fallback to using the GET request method - response = await fetch(url, { - method: 'GET', - }) - } - - if (response.status === 404) { - // page 404'ed, link is invalid - throw new Error('Invalid URL') - } + let regex = new RegExp( + /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi + ) - if (response.status >= 400) { - // some other response came back, set link as invalid - throw new Error('Bad response from server') - } + assert(url && url.match(regex), new createError.BadRequest('Invalid URL')) - isValid = true - } catch (err) { - isValid = false - } + // try a HEAD request first + // this is the fastest way to check as we don't get the whole page back and most servers support this + let response = await fetch(url, { + method: 'HEAD', + }) - return res.status(200).json({ - isValid, + if (response.status >= 400 && response.status !== 404) { + // servers SHOULD respond with a 404 if the page doesn't exist + // this one didn't, so let's fallback to using the GET request method + response = await fetch(url, { + method: 'GET', }) } - default: - return res.status(405).end() + // if page 404'ed, link is invalid + assert(response.status !== 404, new createError.BadRequest('Invalid URL')) + + // some other response came back, set link as invalid + assert( + response.status < 400, + new createError.BadRequest('Bad response from server') + ) + + isValid = true + } catch (err) { + log.debug(err) + isValid = false } + + return res.status(200).json({ + isValid, + }) } + +export default withApiErrorHandler(handler) diff --git a/packages/app/pages/index.tsx b/packages/app/pages/index.tsx index f52f26ec3..7bf675c4f 100644 --- a/packages/app/pages/index.tsx +++ b/packages/app/pages/index.tsx @@ -5,6 +5,7 @@ import UnderMaintenance from '../components/under-maintenance' import { getModelsWithDocumentCount } from '../models/service' import type { Model, ModelCategory } from '../models/types' import { sortModels } from '../utils/sort' +import { getLoggedInUser } from 'auth/user' export interface DashboardPageProps { modelCategories: ModelCategory[] @@ -37,6 +38,17 @@ export function sortModelsIntoCategories(models: Model[]): ModelCategory[] { } export async function getServerSideProps(ctx: NextPageContext) { + // Redirect to sign in page if logged out + //? Unlike other pages, the root path, /meditor doesn't seem to react at all to NextJS middleware + if (!(await getLoggedInUser(ctx.req, ctx.res))) { + return { + redirect: { + destination: '/signin', + permanent: false, + }, + } + } + // TODO: handle error when retrieving models with document count, show user an error message? const [_error, modelsWithDocumentCount] = await getModelsWithDocumentCount() const models = (modelsWithDocumentCount || []).sort(sortModels)