Implemented complete backend API foundation with authentication, document management, blog operations, and admin functionality. Added migration tools for database seeding and document import. **Controllers (4 files):** - auth.controller.js: User authentication (login, getCurrentUser, logout) - documents.controller.js: Document CRUD operations - blog.controller.js: Blog post management with admin/public access - admin.controller.js: Admin dashboard (stats, moderation queue, activity) **Routes (5 files):** - auth.routes.js: Authentication endpoints - documents.routes.js: Document API endpoints - blog.routes.js: Blog API endpoints - admin.routes.js: Admin API endpoints - index.js: Central routing configuration with API documentation **Migration Tools (2 scripts):** - seed-admin.js: Create admin user for system access - migrate-documents.js: Import markdown documents with metadata extraction, slug generation, and dry-run support. Successfully migrated 8 documents from anthropic-submission directory. **Server Updates:** - Integrated all API routes under /api namespace - Updated homepage to reflect completed API implementation - Maintained security middleware (Helmet, CORS, rate limiting) **Testing:** ✅ Server starts successfully on port 9000 ✅ Authentication flow working (login, token validation) ✅ Document endpoints tested (list, get by slug) ✅ Admin stats endpoint verified (requires authentication) ✅ Migration completed: 8 documents imported **Database Status:** - Documents collection: 8 technical papers - Users collection: 1 admin user - All indexes operational This completes the core backend API infrastructure. Next steps: build Tractatus governance services (InstructionClassifier, CrossReferenceValidator, BoundaryEnforcer). 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
265 lines
5.7 KiB
JavaScript
265 lines
5.7 KiB
JavaScript
/**
|
|
* Documents Controller
|
|
* Handles framework documentation CRUD operations
|
|
*/
|
|
|
|
const Document = require('../models/Document.model');
|
|
const { markdownToHtml, extractTOC } = require('../utils/markdown.util');
|
|
const logger = require('../utils/logger.util');
|
|
|
|
/**
|
|
* List all documents
|
|
* GET /api/documents
|
|
*/
|
|
async function listDocuments(req, res) {
|
|
try {
|
|
const { limit = 50, skip = 0, quadrant } = req.query;
|
|
|
|
let documents;
|
|
let total;
|
|
|
|
if (quadrant) {
|
|
documents = await Document.findByQuadrant(quadrant, {
|
|
limit: parseInt(limit),
|
|
skip: parseInt(skip)
|
|
});
|
|
total = await Document.count({ quadrant });
|
|
} else {
|
|
documents = await Document.list({
|
|
limit: parseInt(limit),
|
|
skip: parseInt(skip)
|
|
});
|
|
total = await Document.count();
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
documents,
|
|
pagination: {
|
|
total,
|
|
limit: parseInt(limit),
|
|
skip: parseInt(skip),
|
|
hasMore: parseInt(skip) + documents.length < total
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('List documents error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get document by ID or slug
|
|
* GET /api/documents/:identifier
|
|
*/
|
|
async function getDocument(req, res) {
|
|
try {
|
|
const { identifier } = req.params;
|
|
|
|
// Try to find by ID first, then by slug
|
|
let document;
|
|
if (identifier.match(/^[0-9a-fA-F]{24}$/)) {
|
|
document = await Document.findById(identifier);
|
|
} else {
|
|
document = await Document.findBySlug(identifier);
|
|
}
|
|
|
|
if (!document) {
|
|
return res.status(404).json({
|
|
error: 'Not Found',
|
|
message: 'Document not found'
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
document
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Get document error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Search documents
|
|
* GET /api/documents/search
|
|
*/
|
|
async function searchDocuments(req, res) {
|
|
try {
|
|
const { q, limit = 20, skip = 0 } = req.query;
|
|
|
|
if (!q) {
|
|
return res.status(400).json({
|
|
error: 'Bad Request',
|
|
message: 'Search query (q) is required'
|
|
});
|
|
}
|
|
|
|
const documents = await Document.search(q, {
|
|
limit: parseInt(limit),
|
|
skip: parseInt(skip)
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
query: q,
|
|
documents,
|
|
count: documents.length
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Search documents error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create document (admin only)
|
|
* POST /api/documents
|
|
*/
|
|
async function createDocument(req, res) {
|
|
try {
|
|
const { title, slug, quadrant, persistence, content_markdown, metadata } = req.body;
|
|
|
|
// Convert markdown to HTML
|
|
const content_html = markdownToHtml(content_markdown);
|
|
|
|
// Extract table of contents
|
|
const toc = extractTOC(content_markdown);
|
|
|
|
// Create search index from content
|
|
const search_index = `${title} ${content_markdown}`.toLowerCase();
|
|
|
|
const document = await Document.create({
|
|
title,
|
|
slug,
|
|
quadrant,
|
|
persistence,
|
|
content_html,
|
|
content_markdown,
|
|
toc,
|
|
metadata,
|
|
search_index
|
|
});
|
|
|
|
logger.info(`Document created: ${slug} by ${req.user.email}`);
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
document
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Create document error:', error);
|
|
|
|
// Handle duplicate slug
|
|
if (error.code === 11000) {
|
|
return res.status(409).json({
|
|
error: 'Conflict',
|
|
message: 'A document with this slug already exists'
|
|
});
|
|
}
|
|
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update document (admin only)
|
|
* PUT /api/documents/:id
|
|
*/
|
|
async function updateDocument(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const updates = { ...req.body };
|
|
|
|
// If content_markdown is updated, regenerate HTML and TOC
|
|
if (updates.content_markdown) {
|
|
updates.content_html = markdownToHtml(updates.content_markdown);
|
|
updates.toc = extractTOC(updates.content_markdown);
|
|
updates.search_index = `${updates.title || ''} ${updates.content_markdown}`.toLowerCase();
|
|
}
|
|
|
|
const success = await Document.update(id, updates);
|
|
|
|
if (!success) {
|
|
return res.status(404).json({
|
|
error: 'Not Found',
|
|
message: 'Document not found'
|
|
});
|
|
}
|
|
|
|
const document = await Document.findById(id);
|
|
|
|
logger.info(`Document updated: ${id} by ${req.user.email}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
document
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Update document error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete document (admin only)
|
|
* DELETE /api/documents/:id
|
|
*/
|
|
async function deleteDocument(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const success = await Document.delete(id);
|
|
|
|
if (!success) {
|
|
return res.status(404).json({
|
|
error: 'Not Found',
|
|
message: 'Document not found'
|
|
});
|
|
}
|
|
|
|
logger.info(`Document deleted: ${id} by ${req.user.email}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Document deleted successfully'
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Delete document error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
listDocuments,
|
|
getDocument,
|
|
searchDocuments,
|
|
createDocument,
|
|
updateDocument,
|
|
deleteDocument
|
|
};
|