import _ from 'lodash'; import { Request, Response, NextFunction, Router } from 'express'; import bodyParser from 'body-parser'; import Runner from '../runner'; import logger from '../services/logger'; import { PageNotFoundError } from '@stackbit/dev-common/dist/services/response-errors'; import { APIMethod, APIMethodName } from '../types/api-methods'; const authMiddleware = (apiSecret: string) => { return (req: Request, res: Response, next: NextFunction) => { try { let authorization = req.headers['x-authorization'] || req.headers.authorization; if (Array.isArray(authorization)) { authorization = authorization[0]; } if (!authorization) { throw 'No token provided'; } const token = authorization.split(' ')[1]; if (token === apiSecret) { next(); } else { throw 'Bad token provided'; } } catch (err) { logger.error('not authenticated', { error: err, req: req.path }); res.status(400).send('Not authenticated'); } }; }; export default function routes(router: Router, runner: Runner, apiSecret: string, openUrl: string) { router.use((req, res, next) => { runner.keepAlive(); next(); }); router.post('/_objects', [bodyParser.json({ limit: '5mb' })], (req: Request, res: Response, next: NextFunction) => { const { objectIds, locale } = req.body; return runner .getObjects({ objectIds, locale }) .then((result) => { res.json(result); }) .catch((err) => { logger.error('Error fetching data objects'); logger.debug('error getting fieldData from runner', { error: err.message || err }); if (_.has(err, 'stack')) { logger.debug('error getting fieldData from runner, error.stack: ' + err.stack); } res.status(500).send('Error'); next(err); }); }); router.post('/_objectsWithAnnotations', [bodyParser.json({ limit: '5mb' })], (req: Request, res: Response, next: NextFunction) => { const { annotationTree, clientAnnotationErrors, resolveAllReferences, sourcemaps, locale } = req.body; return runner .getObjectsWithAnnotations({ annotationTree, clientAnnotationErrors, resolveAllReferences, sourcemaps, locale }) .then((result) => { res.json(result); }) .catch((err) => { logger.error('Error fetching data objects with annotations'); logger.debug('error getting fieldData with annotations from runner', { error: err.message || err }); if (_.has(err, 'stack')) { logger.debug('error getting fieldData with annotations from runner, error.stack: ' + err.stack); } res.status(500).send('Error'); next(err); }); }); router.get('/_collections', (req: Request, res: Response, next: NextFunction) => { const { locale } = req.query; return runner .getCollections({ locale: locale as string }) .then((result) => { res.json(result); }) .catch((err) => { logger.error('Error fetching collections'); logger.debug('error getting collections from runner', { error: err.message || err }); if (_.has(err, 'stack')) { logger.debug('error getting collections from runner, error.stack: ' + err.stack); } res.status(500).send('Error'); next(err); }); }); router.get('/_sitemap', (req: Request, res: Response, next: NextFunction) => { const { locale } = req.query; return runner .getSiteMap({ locale: locale as string }) .then((result) => { res.json(result); }) .catch((err) => { logger.error('Error fetching sitemap'); logger.debug('error getting sitemap from runner', { error: err.message || err }); if (_.has(err, 'stack')) { logger.debug('error getting sitemap from runner, error.stack: ' + err.stack); } res.status(500).send('Error'); next(err); }); }); router.get('/_schema', (req: Request, res: Response, next: NextFunction) => { const { locale } = req.query; return runner .getSchema({ locale: locale as string }) .then((schema) => res.json(schema)) .catch((err) => { logger.error('Error fetching schema'); logger.debug('error getting schema from runner', { error: err }); res.status(500).send('Error'); next(err); }); }); router.get('/_pageUrl', (req: Request, res: Response, next: NextFunction) => { const { srcType, locale } = req.query; const srcProjectId = req.query.srcProjectId ?? req.query.spaceId; const srcDocumentId = req.query.srcDocumentId ?? req.query.entryId; runner .getUrl(srcDocumentId as string, srcProjectId as string, srcType as string, locale as string) .then((url) => res.json({ url })) .catch((err) => { if (err !== PageNotFoundError) { logger.error('Error querying for page url'); } const log = err === PageNotFoundError ? logger.debug : logger.warn; log('error getting page path from runner', { params: req.query, error: err }); res.json({ message: err.toString() }); if (err !== PageNotFoundError) { next(err); } }); }); router.get('/_object', (req: Request, res: Response, next: NextFunction) => { const { objectId, projectId } = req.query; return runner .getObject(objectId as string, projectId as string) .then((response) => { res.json(response); }) .catch((err) => { logger.error('Error fetching object'); logger.debug('error getting object', { error: err }); next(err); }); }); router.get('/_assets', [bodyParser.json({ limit: '500kb' })], (req: Request, res: Response, next: NextFunction) => { const filterParams = _.pick(req.body, ['searchQuery', 'pageId', 'pageSize']); return runner .listAssets(filterParams) .then((response) => { res.json(response); }) .catch((err) => { logger.error('Error listing assets'); logger.debug('error listing assets', { error: err }); res.status(500).json({ message: err.toString() }); next(err); }); }); router.get('/_health', (req: Request, res: Response) => { return res.status(200).json({ status: 'ok' }); }); router.post('/_action', [authMiddleware(apiSecret), bodyParser.json({ limit: '50mb' })], (req: Request, res: Response, next: NextFunction) => { const { action, data } = req.body; return runner .makeAction(action, data) .then((result) => { res.json(result); }) .catch((err) => { logger.error('Error performing action: ' + action); logger.debug('error performing action', { action, error: err.message || err }); if (_.has(err, 'stack')) { logger.debug('error.stack: ' + err.stack); } res.status(500).json({ message: err.message || err }); next(err); }); }); router.get('/_stackbit', (req, res) => { return res.redirect(302, openUrl); }); router.post( '/_stackbit/:method', [authMiddleware(apiSecret), bodyParser.json({ limit: '50mb' })], (req: Request<{ method: T }>, res: Response, next: NextFunction) => { const method: APIMethodName = req.params.method; if (!runner[method]) { const message = `method "/_stackbit/${method}" is not supported`; logger.error(message); res.status(400).send({ message }); next(new Error(message)); return; } const data = req.body; return runner[method](data) .then((result: APIMethod['result']) => { res.json(result); }) .catch((err) => { logger.error(`error executing ${method} method`, { data, error: err.message || err }); if (_.has(err, 'stack')) { logger.debug('error.stack: ' + err.stack); } res.status(500).send({ message: err.message || err }); next(err); }); } ); // For stackbit dev only when to develop and debug content-source webhooks router.post( '/_stackbit/onWebhook/:srcType/:srcProjectId', bodyParser.json({ limit: '50mb' }), (req: Request<{ srcType: string; srcProjectId: string }>, res: Response, next: NextFunction) => { const data = req.body; return runner .onWebhook({ srcType: req.params.srcType, srcProjectId: req.params.srcProjectId, data: req.body, headers: req.headers as Record }) .then((result: APIMethod['result']) => { res.json(result); }) .catch((err) => { logger.error(`error executing onWebhook method`, { data, error: err.message || err }); if (_.has(err, 'stack')) { logger.debug('error.stack: ' + err.stack); } res.status(500).send({ message: err.message || err }); next(err); }); } ); }