Files
wekan/server/apiMiddleware.js
T
Lauri OjansivuandClaude Opus 4.8 fa2cb91694 Fix #5804: REST API returns HTTP 400/404 instead of 500 for invalid requests
Posting a comment without the required `comment` parameter (or to a board
that does not exist) returned an HTTP 500 error page. The schema-validation
error thrown on insert is a circular object (SimpleSchemaValidationContext ->
SimpleSchema -> ...), and serializing it crashed the response writer with
"Converting circular structure to JSON".

- Add Meteor-free helpers (server/lib/apiResponseHelpers.js): safeJsonStringify
  (crash-proof, falls back to {error} on circular structures), httpStatusForError
  (maps to 401/403/404/500), validateCommentBody, extractErrorMessage.
- sendJsonResult now serializes via safeJsonStringify so a non-serializable
  payload can no longer crash into a 500.
- checkBoardAccess/checkBoardWriteAccess/checkBoardAdmin throw a 404 instead of
  a TypeError on board.members when the board does not exist.
- POST comment validates input (400 when comment missing); REST comment catch
  blocks return the real status code instead of 200.
- Add 18 unit tests (server/lib/tests/apiResponseHelpers.tests.js).
- Update CHANGELOG upcoming.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 07:32:42 +03:00

106 lines
3.9 KiB
JavaScript

// API middleware: body parsing, auth token handling, API gate, and JSON response helper.
// Replaces communitypackages:json-routes middleware chain.
// Must be imported before model files that register API routes.
const { Meteor } = require('meteor/meteor');
const { Accounts } = require('meteor/accounts-base');
const { WebApp } = require('meteor/webapp');
const bodyParser = require('body-parser');
const { safeJsonStringify } = require('/server/lib/apiResponseHelpers');
// ---------------------------------------------------------------------------
// 1. Body parsing (previously registered by json-routes)
// ---------------------------------------------------------------------------
WebApp.handlers.use(bodyParser.urlencoded({ limit: '50mb', extended: false }));
WebApp.handlers.use(bodyParser.json({ limit: '50mb' }));
// ---------------------------------------------------------------------------
// 2. API gate — check WITH_API env var (previously in models/users.js)
// ---------------------------------------------------------------------------
WebApp.handlers.use(function apiGate(req, res, next) {
const api = req.url.startsWith('/api');
if ((api && process.env.WITH_API === 'true') || !api) {
return next();
}
res.writeHead(301, { Location: '/' });
return res.end();
});
// ---------------------------------------------------------------------------
// 3. Bearer token parser (replaces communitypackages:rest-bearer-token-parser)
// ---------------------------------------------------------------------------
WebApp.handlers.use(function parseBearerToken(req, res, next) {
// Check Authorization header first
const authHeader = req.headers.authorization;
if (authHeader) {
const match = authHeader.match(/^Bearer\s+(.+)$/i);
if (match) {
req.authToken = match[1];
}
}
// Fallback to access_token query param
if (!req.authToken && req.query && req.query.access_token) {
req.authToken = req.query.access_token;
}
next();
});
// ---------------------------------------------------------------------------
// 4. User authentication (replaces communitypackages:authenticate-user-by-token)
// ---------------------------------------------------------------------------
WebApp.handlers.use(async function authenticateByToken(req, res, next) {
if (req.authToken) {
try {
const hashedToken = Accounts._hashLoginToken(req.authToken);
const user = await Meteor.users.findOneAsync(
{ 'services.resume.loginTokens.hashedToken': hashedToken },
{ fields: { _id: 1 } },
);
if (user) {
req.userId = user._id;
}
} catch (e) {
// Ignore auth errors — routes handle missing userId themselves
}
}
next();
});
// ---------------------------------------------------------------------------
// 5. sendJsonResult — drop-in replacement for JsonRoutes.sendResult
// ---------------------------------------------------------------------------
function sendJsonResult(res, options) {
options = options || {};
// Default response headers (matching json-routes behavior)
res.setHeader('Cache-Control', 'no-store');
res.setHeader('Pragma', 'no-cache');
// Custom headers
if (options.headers) {
Object.entries(options.headers).forEach(function ([key, value]) {
res.setHeader(key, value);
});
}
// Status code
res.statusCode = options.code || 200;
// JSON body
if (options.data !== undefined) {
const shouldPrettyPrint = process.env.NODE_ENV === 'development';
const spacer = shouldPrettyPrint ? 2 : null;
res.setHeader('Content-Type', 'application/json');
// Use a crash-proof serializer: some error payloads (e.g. a circular
// SimpleSchema validation error) would otherwise throw inside
// JSON.stringify and surface as a generic HTTP 500. See #5804.
res.write(safeJsonStringify(options.data, spacer));
}
res.end();
}
module.exports = { sendJsonResult };