tractatus/src/routes/documents.routes.js
TheFlow 79a280a403 feat(security): implement document publish workflow with safe defaults
SECURITY IMPROVEMENTS:
- Change default visibility from 'public' to 'internal' (prevents accidental exposure)
- Add visibility validation (public/internal/confidential/archived)
- Require valid category for public documents
- Add workflow_status tracking (draft/review/published)

PUBLISH WORKFLOW:
- New Document.publish(id, options) method with comprehensive validation
- New Document.unpublish(id, reason) method with audit trail
- New Document.listByWorkflowStatus(status) for workflow management

API ENDPOINTS (Admin only):
- POST /api/documents/:id/publish - Explicit publish with category validation
- POST /api/documents/:id/unpublish - Revert to internal with reason
- GET /api/documents/drafts - List unpublished documents

WORLD-CLASS UX:
- Clear validation messages with actionable guidance
- Lists available categories in error messages
- Tracks publish/unpublish history for audit trail

BACKWARD COMPATIBLE:
- Existing public documents unaffected
- Migration scripts automatically use safer defaults

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 12:34:51 +13:00

100 lines
2.5 KiB
JavaScript

/**
* Documents Routes
* Framework documentation endpoints
*/
const express = require('express');
const router = express.Router();
const documentsController = require('../controllers/documents.controller');
const { authenticateToken, requireRole } = 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
router.get('/archived',
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 (ID or slug)
router.get('/:identifier',
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)
);
module.exports = router;