mirror of
https://github.com/wekan/wekan.git
synced 2026-11-05 15:45:01 -05:00
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>
106 lines
3.9 KiB
JavaScript
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 };
|