tractatus/src/routes/documents.routes.js
TheFlow 00d89ce095 fix: add visibility check to getDocument/getTranslations endpoints
Non-public documents (internal, archived, confidential) were accessible
to unauthenticated users via direct slug/ID lookup. List and search
endpoints already filtered for visibility: 'public', but the individual
document endpoints did not. Added optionalAuth middleware and visibility
checks so non-public docs return 404 to public users while remaining
accessible to admin users.

Also adds Guardian Agents translations to village-case-study locale
files (DE, FR, MI) — 8 new keys per locale, flow step renumbered
6→7→8 with new Guardian Agents verification step at position 6.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 17:27:48 +13:00

128 lines
3.3 KiB
JavaScript

/**
* Documents Routes
* Framework documentation endpoints
*/
const express = require('express');
const router = express.Router();
const documentsController = require('../controllers/documents.controller');
const { authenticateToken, requireRole, optionalAuth } = require('../middleware/auth.middleware');
const { validateRequired, validateObjectId, validateSlug } = require('../middleware/validation.middleware');
const { asyncHandler } = require('../middleware/error.middleware');
/**
* Public routes (read-only)
*/
// GET /api/documents/search?q=query
router.get('/search',
asyncHandler(documentsController.searchDocuments)
);
// GET /api/documents/archived (admin only)
router.get('/archived',
authenticateToken,
requireRole('admin'),
asyncHandler(documentsController.listArchivedDocuments)
);
// GET /api/documents/drafts (admin only)
router.get('/drafts',
authenticateToken,
requireRole('admin'),
asyncHandler(documentsController.listDraftDocuments)
);
// GET /api/documents
router.get('/', (req, res, next) => {
// Redirect browser requests to API documentation
const acceptsHtml = req.accepts('html');
const acceptsJson = req.accepts('json');
if (acceptsHtml && !acceptsJson) {
return res.redirect(302, '/api-reference.html#documents');
}
next();
}, asyncHandler(documentsController.listDocuments));
// GET /api/documents/:identifier/translations (public, visibility-filtered)
router.get('/:identifier/translations',
optionalAuth,
asyncHandler(documentsController.getTranslations)
);
// GET /api/documents/:identifier (ID or slug, visibility-filtered)
router.get('/:identifier',
optionalAuth,
asyncHandler(documentsController.getDocument)
);
/**
* Admin routes (protected)
*/
// POST /api/documents
router.post('/',
authenticateToken,
requireRole('admin'),
validateRequired(['title', 'slug', 'quadrant', 'content_markdown']),
validateSlug,
asyncHandler(documentsController.createDocument)
);
// PUT /api/documents/:id
router.put('/:id',
authenticateToken,
requireRole('admin'),
validateObjectId('id'),
asyncHandler(documentsController.updateDocument)
);
// DELETE /api/documents/:id
router.delete('/:id',
authenticateToken,
requireRole('admin'),
validateObjectId('id'),
asyncHandler(documentsController.deleteDocument)
);
// POST /api/documents/:id/publish (admin only)
// SECURITY: Explicit publish workflow with validation
router.post('/:id/publish',
authenticateToken,
requireRole('admin'),
validateObjectId('id'),
validateRequired(['category']),
asyncHandler(documentsController.publishDocument)
);
// POST /api/documents/:id/unpublish (admin only)
router.post('/:id/unpublish',
authenticateToken,
requireRole('admin'),
validateObjectId('id'),
asyncHandler(documentsController.unpublishDocument)
);
// POST /api/documents/:id/translate (admin only)
// Translate document to target language using DeepL
router.post('/:id/translate',
authenticateToken,
requireRole('admin'),
validateObjectId('id'),
validateRequired(['targetLang']),
asyncHandler(documentsController.translateDocument)
);
// DELETE /api/documents/:id/translations/:lang (admin only)
// Delete a translation
router.delete('/:id/translations/:lang',
authenticateToken,
requireRole('admin'),
validateObjectId('id'),
asyncHandler(documentsController.deleteTranslation)
);
module.exports = router;