feat: add API routes, controllers, and migration tools

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>
This commit is contained in:
TheFlow 2025-10-07 00:36:40 +13:00
parent 067012ad24
commit 0d75492c60
12 changed files with 1782 additions and 19 deletions

342
scripts/migrate-documents.js Executable file
View file

@ -0,0 +1,342 @@
#!/usr/bin/env node
/**
* Document Migration Script
* Migrates markdown documents into the MongoDB database
*
* Usage:
* npm run migrate:docs # Interactive mode
* node scripts/migrate-documents.js --source /path/to/docs --dry-run
* node scripts/migrate-documents.js --source /path/to/docs --force
*/
require('dotenv').config();
const fs = require('fs').promises;
const path = require('path');
const { connect, close } = require('../src/utils/db.util');
const Document = require('../src/models/Document.model');
const { markdownToHtml, extractTOC, generateSlug } = require('../src/utils/markdown.util');
const logger = require('../src/utils/logger.util');
// Parse command line arguments
const args = process.argv.slice(2);
const sourceArg = args.indexOf('--source');
const dryRun = args.includes('--dry-run');
const force = args.includes('--force');
// Default source paths
const DEFAULT_SOURCES = [
'/home/theflow/projects/tractatus/docs/markdown',
'/home/theflow/projects/sydigital/stochastic/innovation-exploration/anthropic-submission'
];
/**
* Extract front matter from markdown
*/
function extractFrontMatter(content) {
const frontMatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
const match = content.match(frontMatterRegex);
if (!match) {
return { frontMatter: {}, content };
}
const frontMatterText = match[1];
const remainingContent = match[2];
// Parse YAML-like front matter
const frontMatter = {};
frontMatterText.split('\n').forEach(line => {
const [key, ...valueParts] = line.split(':');
if (key && valueParts.length > 0) {
const value = valueParts.join(':').trim();
frontMatter[key.trim()] = value.replace(/^["']|["']$/g, ''); // Remove quotes
}
});
return { frontMatter, content: remainingContent };
}
/**
* Extract metadata from filename and content
*/
function extractMetadata(filename, content, frontMatter) {
// Try to extract document identifier from filename
// Patterns: TRA-VAL-0001, STO-INN-0010, etc.
const identifierMatch = filename.match(/([A-Z]{3}-[A-Z]{3}-\d{4})/);
const identifier = identifierMatch ? identifierMatch[1] : null;
// Extract quadrant from identifier
let quadrant = null;
if (identifier) {
const [quad] = identifier.split('-');
const quadrantMap = {
'STR': 'strategic',
'OPS': 'operational',
'TAC': 'tactical',
'SYS': 'system',
'STO': 'stochastic'
};
quadrant = quadrantMap[quad] || null;
}
// Extract title from first H1 or front matter
let title = frontMatter.title || null;
if (!title) {
const h1Match = content.match(/^#\s+(.+)$/m);
title = h1Match ? h1Match[1] : path.basename(filename, '.md');
}
// Extract version from identifier or front matter
let version = frontMatter.version || '1.0';
if (identifier && identifier.match(/v(\d+-\d+)/)) {
version = identifier.match(/v(\d+-\d+)/)[1].replace('-', '.');
}
// Determine document type
let type = frontMatter.type || 'governance';
if (filename.includes('technical-proposal')) type = 'technical';
else if (filename.includes('appendix')) type = 'technical';
else if (filename.includes('framework')) type = 'framework';
else if (filename.includes('whitepaper')) type = 'research';
else if (filename.includes('case-stud')) type = 'case-study';
// Extract author
const author = frontMatter.author || 'System';
// Extract tags
const tags = frontMatter.tags
? frontMatter.tags.split(',').map(t => t.trim())
: [];
return {
identifier,
title,
type,
quadrant,
version,
author,
tags,
status: 'published'
};
}
/**
* Process a single markdown file
*/
async function processMarkdownFile(filePath, sourcePath) {
const filename = path.basename(filePath);
const rawContent = await fs.readFile(filePath, 'utf-8');
// Extract front matter
const { frontMatter, content } = extractFrontMatter(rawContent);
// Extract metadata
const metadata = extractMetadata(filename, content, frontMatter);
// Convert to HTML
const htmlContent = markdownToHtml(content);
// Extract table of contents
const tableOfContents = extractTOC(content);
// Generate slug from title
const slug = generateSlug(metadata.title);
// Build document object matching Document model schema
const doc = {
title: metadata.title,
slug: slug,
quadrant: metadata.quadrant,
persistence: 'HIGH', // Default for technical documents
content_html: htmlContent,
content_markdown: content,
toc: tableOfContents,
metadata: {
author: metadata.author,
version: metadata.version,
document_code: metadata.identifier,
tags: metadata.tags,
original_filename: filename,
source_path: path.relative(sourcePath, filePath),
migrated_at: new Date()
},
search_index: content.toLowerCase(),
translations: {},
download_formats: {}
};
return doc;
}
/**
* Find all markdown files in directory
*/
async function findMarkdownFiles(dirPath) {
const files = [];
async function scan(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// Skip node_modules and hidden directories
if (!entry.name.startsWith('.') && entry.name !== 'node_modules') {
await scan(fullPath);
}
} else if (entry.isFile() && entry.name.endsWith('.md')) {
// Skip README files
if (!entry.name.toLowerCase().includes('readme')) {
files.push(fullPath);
}
}
}
}
await scan(dirPath);
return files;
}
/**
* Main migration function
*/
async function migrate() {
try {
console.log('\n=== Tractatus Document Migration ===\n');
// Determine source path
let sourcePath;
if (sourceArg !== -1 && args[sourceArg + 1]) {
sourcePath = args[sourceArg + 1];
} else {
// Check default sources
for (const defaultPath of DEFAULT_SOURCES) {
try {
const stat = await fs.stat(defaultPath);
if (stat.isDirectory()) {
const files = await fs.readdir(defaultPath);
if (files.length > 0) {
sourcePath = defaultPath;
break;
}
}
} catch (err) {
// Path doesn't exist, try next
}
}
}
if (!sourcePath) {
console.error('❌ No source path specified and no documents found in default locations.');
console.log('\nUsage: npm run migrate:docs -- --source /path/to/docs');
console.log('\nDefault locations checked:');
DEFAULT_SOURCES.forEach(p => console.log(` - ${p}`));
process.exit(1);
}
console.log(`📂 Source: ${sourcePath}`);
console.log(`🔍 Mode: ${dryRun ? 'DRY RUN (no changes)' : 'MIGRATION (will write to database)'}`);
console.log('');
// Find markdown files
const markdownFiles = await findMarkdownFiles(sourcePath);
if (markdownFiles.length === 0) {
console.log('⚠️ No markdown files found.');
process.exit(0);
}
console.log(`Found ${markdownFiles.length} markdown file(s):\n`);
markdownFiles.forEach((file, i) => {
console.log(` ${i + 1}. ${path.relative(sourcePath, file)}`);
});
console.log('');
if (!dryRun) {
// Connect to database
await connect();
}
// Process each file
let createdCount = 0;
let updatedCount = 0;
let skippedCount = 0;
let errorsCount = 0;
for (const filePath of markdownFiles) {
try {
const doc = await processMarkdownFile(filePath, sourcePath);
const filename = path.basename(filePath);
if (dryRun) {
console.log(`✓ [DRY RUN] ${filename}`);
console.log(` Title: ${doc.title}`);
console.log(` Slug: ${doc.slug}`);
console.log(` Quadrant: ${doc.quadrant || 'none'}`);
console.log(` Code: ${doc.metadata.document_code || 'none'}`);
console.log('');
createdCount++;
} else {
// Check if document already exists by slug
const existing = await Document.findBySlug(doc.slug);
if (existing && !force) {
console.log(`⊘ SKIPPED ${filename} (already exists: ${existing.slug})`);
skippedCount++;
} else if (existing && force) {
// Update existing document
const updatedDoc = await Document.update(existing._id, doc);
console.log(`↻ UPDATED ${filename} (${updatedDoc.slug})`);
updatedCount++;
} else {
// Create new document
const createdDoc = await Document.create(doc);
console.log(`✓ CREATED ${filename} (${createdDoc.slug})`);
createdCount++;
}
}
} catch (error) {
console.error(`✗ ERROR processing ${path.basename(filePath)}: ${error.message}`);
logger.error(`Migration error for ${filePath}:`, error);
errorsCount++;
}
}
// Summary
console.log('\n=== Migration Summary ===\n');
console.log(` Total files: ${markdownFiles.length}`);
console.log(` Created: ${createdCount}`);
console.log(` Updated: ${updatedCount}`);
console.log(` Skipped: ${skippedCount}`);
console.log(` Errors: ${errorsCount}`);
console.log('');
if (dryRun) {
console.log('💡 This was a dry run. No changes were made.');
console.log(' Run without --dry-run to perform actual migration.');
}
if (!dryRun) {
logger.info(`Document migration completed: ${createdCount} created, ${updatedCount} updated, ${skippedCount} skipped, ${errorsCount} errors`);
}
} catch (error) {
console.error('\n❌ Migration failed:', error.message);
logger.error('Migration error:', error);
process.exit(1);
} finally {
if (!dryRun) {
await close();
}
}
}
// Run if called directly
if (require.main === module) {
migrate();
}
module.exports = migrate;

113
scripts/seed-admin.js Executable file
View file

@ -0,0 +1,113 @@
#!/usr/bin/env node
/**
* Seed Admin User Script
* Creates the initial admin user for the Tractatus platform
*
* Usage: npm run seed:admin
*/
require('dotenv').config();
const readline = require('readline');
const { connect, close } = require('../src/utils/db.util');
const User = require('../src/models/User.model');
const logger = require('../src/utils/logger.util');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
function question(prompt) {
return new Promise((resolve) => {
rl.question(prompt, resolve);
});
}
async function seedAdmin() {
try {
console.log('\n=== Tractatus Admin User Setup ===\n');
// Connect to database
await connect();
// Check if admin user already exists
const existingAdmin = await User.findByEmail(process.env.ADMIN_EMAIL || 'admin@tractatus.local');
if (existingAdmin) {
console.log('⚠️ Admin user already exists.');
const overwrite = await question('Do you want to delete and recreate? (yes/no): ');
if (overwrite.toLowerCase() !== 'yes') {
console.log('Cancelled. No changes made.');
await cleanup();
return;
}
await User.deleteOne({ _id: existingAdmin._id });
console.log('✅ Existing admin user deleted.');
}
// Get admin details
console.log('\nEnter admin user details:');
const name = await question('Name (default: Admin User): ') || 'Admin User';
const email = await question(`Email (default: ${process.env.ADMIN_EMAIL || 'admin@tractatus.local'}): `)
|| process.env.ADMIN_EMAIL
|| 'admin@tractatus.local';
// Password input (hidden)
console.log('\n⚠ Password will be visible. Use a development password only.');
const password = await question('Password (min 8 chars): ');
if (!password || password.length < 8) {
console.error('❌ Password must be at least 8 characters.');
await cleanup();
return;
}
// Create admin user
const admin = await User.create({
name,
email,
password, // Will be hashed by the model
role: 'admin',
active: true
});
console.log('\n✅ Admin user created successfully!');
console.log('\nCredentials:');
console.log(` Email: ${admin.email}`);
console.log(` Role: ${admin.role}`);
console.log(` ID: ${admin._id}`);
console.log('\nYou can now login at: POST /api/auth/login');
console.log('');
logger.info(`Admin user created: ${admin.email}`);
} catch (error) {
console.error('\n❌ Error creating admin user:', error.message);
logger.error('Admin seed error:', error);
process.exit(1);
} finally {
await cleanup();
}
}
async function cleanup() {
rl.close();
await close();
}
// Handle Ctrl+C
process.on('SIGINT', async () => {
console.log('\n\n👋 Cancelled by user');
await cleanup();
process.exit(0);
});
// Run if called directly
if (require.main === module) {
seedAdmin();
}
module.exports = seedAdmin;

View file

@ -0,0 +1,289 @@
/**
* Admin Controller
* Moderation queue and system statistics
*/
const ModerationQueue = require('../models/ModerationQueue.model');
const Document = require('../models/Document.model');
const BlogPost = require('../models/BlogPost.model');
const User = require('../models/User.model');
const logger = require('../utils/logger.util');
/**
* Get moderation queue dashboard
* GET /api/admin/moderation
*/
async function getModerationQueue(req, res) {
try {
const { limit = 20, skip = 0, priority, quadrant, item_type } = req.query;
let items;
let total;
if (quadrant) {
items = await ModerationQueue.findByQuadrant(quadrant, {
limit: parseInt(limit),
skip: parseInt(skip)
});
total = await ModerationQueue.countPending({ quadrant });
} else if (item_type) {
items = await ModerationQueue.findByType(item_type, {
limit: parseInt(limit),
skip: parseInt(skip)
});
total = await ModerationQueue.countPending({ item_type });
} else {
items = await ModerationQueue.findPending({
limit: parseInt(limit),
skip: parseInt(skip),
priority
});
total = await ModerationQueue.countPending(priority ? { priority } : {});
}
// Get stats by quadrant
const stats = await ModerationQueue.getStatsByQuadrant();
res.json({
success: true,
items,
stats: stats.reduce((acc, stat) => {
acc[stat._id] = {
total: stat.count,
high_priority: stat.high_priority
};
return acc;
}, {}),
pagination: {
total,
limit: parseInt(limit),
skip: parseInt(skip)
}
});
} catch (error) {
logger.error('Get moderation queue error:', error);
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
/**
* Get single moderation item
* GET /api/admin/moderation/:id
*/
async function getModerationItem(req, res) {
try {
const { id } = req.params;
const item = await ModerationQueue.findById(id);
if (!item) {
return res.status(404).json({
error: 'Not Found',
message: 'Moderation item not found'
});
}
res.json({
success: true,
item
});
} catch (error) {
logger.error('Get moderation item error:', error);
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
/**
* Review moderation item (approve/reject/escalate)
* POST /api/admin/moderation/:id/review
*/
async function reviewModerationItem(req, res) {
try {
const { id } = req.params;
const { action, notes } = req.body;
const item = await ModerationQueue.findById(id);
if (!item) {
return res.status(404).json({
error: 'Not Found',
message: 'Moderation item not found'
});
}
let success;
switch (action) {
case 'approve':
success = await ModerationQueue.approve(id, req.userId, notes);
break;
case 'reject':
success = await ModerationQueue.reject(id, req.userId, notes);
break;
case 'escalate':
success = await ModerationQueue.escalate(id, req.userId, notes);
break;
default:
return res.status(400).json({
error: 'Bad Request',
message: 'Invalid action. Must be: approve, reject, or escalate'
});
}
if (!success) {
return res.status(500).json({
error: 'Internal Server Error',
message: 'Failed to update moderation item'
});
}
const updatedItem = await ModerationQueue.findById(id);
logger.info(`Moderation item ${action}: ${id} by ${req.user.email}`);
res.json({
success: true,
item: updatedItem,
message: `Item ${action}d successfully`
});
} catch (error) {
logger.error('Review moderation item error:', error);
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
/**
* Get system statistics
* GET /api/admin/stats
*/
async function getSystemStats(req, res) {
try {
// Document stats
const totalDocuments = await Document.count();
const documentsByQuadrant = await Promise.all([
Document.count({ quadrant: 'STRATEGIC' }),
Document.count({ quadrant: 'OPERATIONAL' }),
Document.count({ quadrant: 'TACTICAL' }),
Document.count({ quadrant: 'SYSTEM' }),
Document.count({ quadrant: 'STOCHASTIC' })
]);
// Blog stats
const blogStats = await Promise.all([
BlogPost.countByStatus('published'),
BlogPost.countByStatus('draft'),
BlogPost.countByStatus('pending')
]);
// Moderation queue stats
const moderationStats = await ModerationQueue.getStatsByQuadrant();
const totalPending = await ModerationQueue.countPending();
// User stats
const totalUsers = await User.count();
const activeUsers = await User.count({ active: true });
res.json({
success: true,
stats: {
documents: {
total: totalDocuments,
by_quadrant: {
STRATEGIC: documentsByQuadrant[0],
OPERATIONAL: documentsByQuadrant[1],
TACTICAL: documentsByQuadrant[2],
SYSTEM: documentsByQuadrant[3],
STOCHASTIC: documentsByQuadrant[4]
}
},
blog: {
published: blogStats[0],
draft: blogStats[1],
pending: blogStats[2],
total: blogStats[0] + blogStats[1] + blogStats[2]
},
moderation: {
total_pending: totalPending,
by_quadrant: moderationStats.reduce((acc, stat) => {
acc[stat._id] = {
total: stat.count,
high_priority: stat.high_priority
};
return acc;
}, {})
},
users: {
total: totalUsers,
active: activeUsers,
inactive: totalUsers - activeUsers
}
}
});
} catch (error) {
logger.error('Get system stats error:', error);
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
/**
* Get recent activity log
* GET /api/admin/activity
*/
async function getActivityLog(req, res) {
try {
// This would typically read from a dedicated activity log
// For now, return recent moderation reviews as example
const { limit = 50 } = req.query;
const collection = await require('../utils/db.util').getCollection('moderation_queue');
const recentActivity = await collection
.find({ status: 'reviewed' })
.sort({ reviewed_at: -1 })
.limit(parseInt(limit))
.toArray();
res.json({
success: true,
activity: recentActivity.map(item => ({
timestamp: item.reviewed_at,
action: item.review_decision?.action,
item_type: item.item_type,
item_id: item.item_id,
reviewer: item.review_decision?.reviewer,
notes: item.review_decision?.notes
}))
});
} catch (error) {
logger.error('Get activity log error:', error);
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
module.exports = {
getModerationQueue,
getModerationItem,
reviewModerationItem,
getSystemStats,
getActivityLog
};

View file

@ -0,0 +1,121 @@
/**
* Authentication Controller
* Handles user login and token verification
*/
const User = require('../models/User.model');
const { generateToken } = require('../utils/jwt.util');
const logger = require('../utils/logger.util');
/**
* Login user
* POST /api/auth/login
*/
async function login(req, res) {
try {
const { email, password } = req.body;
// Authenticate user
const user = await User.authenticate(email, password);
if (!user) {
logger.warn(`Failed login attempt for email: ${email}`);
return res.status(401).json({
error: 'Authentication failed',
message: 'Invalid email or password'
});
}
// Generate JWT token
const token = generateToken({
userId: user._id.toString(),
email: user.email,
role: user.role
});
logger.info(`User logged in: ${user.email}`);
res.json({
success: true,
token,
user: {
id: user._id,
email: user.email,
name: user.name,
role: user.role
}
});
} catch (error) {
logger.error('Login error:', error);
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred during login'
});
}
}
/**
* Verify token and get current user
* GET /api/auth/me
*/
async function getCurrentUser(req, res) {
try {
// User is already attached to req by auth middleware
const user = await User.findById(req.userId);
if (!user) {
return res.status(404).json({
error: 'Not Found',
message: 'User not found'
});
}
res.json({
success: true,
user: {
id: user._id,
email: user.email,
name: user.name,
role: user.role,
created_at: user.created_at,
last_login: user.last_login
}
});
} catch (error) {
logger.error('Get current user error:', error);
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
/**
* Logout (client-side token removal, server logs it)
* POST /api/auth/logout
*/
async function logout(req, res) {
try {
logger.info(`User logged out: ${req.user.email}`);
res.json({
success: true,
message: 'Logged out successfully'
});
} catch (error) {
logger.error('Logout error:', error);
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
module.exports = {
login,
getCurrentUser,
logout
};

View file

@ -0,0 +1,332 @@
/**
* Blog Controller
* AI-curated blog with human oversight
*/
const BlogPost = require('../models/BlogPost.model');
const { markdownToHtml } = require('../utils/markdown.util');
const logger = require('../utils/logger.util');
/**
* List published blog posts (public)
* GET /api/blog
*/
async function listPublishedPosts(req, res) {
try {
const { limit = 10, skip = 0 } = req.query;
const posts = await BlogPost.findPublished({
limit: parseInt(limit),
skip: parseInt(skip)
});
const total = await BlogPost.countByStatus('published');
res.json({
success: true,
posts,
pagination: {
total,
limit: parseInt(limit),
skip: parseInt(skip),
hasMore: parseInt(skip) + posts.length < total
}
});
} catch (error) {
logger.error('List published posts error:', error);
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
/**
* Get single blog post by slug (public, published only)
* GET /api/blog/:slug
*/
async function getPublishedPost(req, res) {
try {
const { slug } = req.params;
const post = await BlogPost.findBySlug(slug);
if (!post || post.status !== 'published') {
return res.status(404).json({
error: 'Not Found',
message: 'Blog post not found'
});
}
// Increment view count
await BlogPost.incrementViews(post._id);
res.json({
success: true,
post
});
} catch (error) {
logger.error('Get published post error:', error);
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
/**
* List posts by status (admin only)
* GET /api/blog/admin/posts?status=draft
*/
async function listPostsByStatus(req, res) {
try {
const { status = 'draft', limit = 20, skip = 0 } = req.query;
const posts = await BlogPost.findByStatus(status, {
limit: parseInt(limit),
skip: parseInt(skip)
});
const total = await BlogPost.countByStatus(status);
res.json({
success: true,
status,
posts,
pagination: {
total,
limit: parseInt(limit),
skip: parseInt(skip)
}
});
} catch (error) {
logger.error('List posts by status error:', error);
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
/**
* Get any post by ID (admin only)
* GET /api/blog/admin/:id
*/
async function getPostById(req, res) {
try {
const { id } = req.params;
const post = await BlogPost.findById(id);
if (!post) {
return res.status(404).json({
error: 'Not Found',
message: 'Blog post not found'
});
}
res.json({
success: true,
post
});
} catch (error) {
logger.error('Get post by ID error:', error);
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
/**
* Create blog post (admin only)
* POST /api/blog
*/
async function createPost(req, res) {
try {
const { title, slug, content, excerpt, tags, author, tractatus_classification } = req.body;
// Convert markdown content to HTML if needed
const content_html = content.includes('# ') ? markdownToHtml(content) : content;
const post = await BlogPost.create({
title,
slug,
content: content_html,
excerpt,
tags,
author: {
...author,
name: author?.name || req.user.name || req.user.email
},
tractatus_classification,
status: 'draft'
});
logger.info(`Blog post created: ${slug} by ${req.user.email}`);
res.status(201).json({
success: true,
post
});
} catch (error) {
logger.error('Create post error:', error);
// Handle duplicate slug
if (error.code === 11000) {
return res.status(409).json({
error: 'Conflict',
message: 'A post with this slug already exists'
});
}
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
/**
* Update blog post (admin only)
* PUT /api/blog/:id
*/
async function updatePost(req, res) {
try {
const { id } = req.params;
const updates = { ...req.body };
// If content is updated and looks like markdown, convert to HTML
if (updates.content && updates.content.includes('# ')) {
updates.content = markdownToHtml(updates.content);
}
const success = await BlogPost.update(id, updates);
if (!success) {
return res.status(404).json({
error: 'Not Found',
message: 'Blog post not found'
});
}
const post = await BlogPost.findById(id);
logger.info(`Blog post updated: ${id} by ${req.user.email}`);
res.json({
success: true,
post
});
} catch (error) {
logger.error('Update post error:', error);
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
/**
* Publish blog post (admin only)
* POST /api/blog/:id/publish
*/
async function publishPost(req, res) {
try {
const { id } = req.params;
const { review_notes } = req.body;
const post = await BlogPost.findById(id);
if (!post) {
return res.status(404).json({
error: 'Not Found',
message: 'Blog post not found'
});
}
if (post.status === 'published') {
return res.status(400).json({
error: 'Bad Request',
message: 'Post is already published'
});
}
// Update with review notes if provided
if (review_notes) {
await BlogPost.update(id, {
'moderation.review_notes': review_notes
});
}
// Publish the post
await BlogPost.publish(id, req.userId);
const updatedPost = await BlogPost.findById(id);
logger.info(`Blog post published: ${id} by ${req.user.email}`);
res.json({
success: true,
post: updatedPost,
message: 'Post published successfully'
});
} catch (error) {
logger.error('Publish post error:', error);
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
/**
* Delete blog post (admin only)
* DELETE /api/blog/:id
*/
async function deletePost(req, res) {
try {
const { id } = req.params;
const success = await BlogPost.delete(id);
if (!success) {
return res.status(404).json({
error: 'Not Found',
message: 'Blog post not found'
});
}
logger.info(`Blog post deleted: ${id} by ${req.user.email}`);
res.json({
success: true,
message: 'Post deleted successfully'
});
} catch (error) {
logger.error('Delete post error:', error);
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
module.exports = {
listPublishedPosts,
getPublishedPost,
listPostsByStatus,
getPostById,
createPost,
updatePost,
publishPost,
deletePost
};

View file

@ -0,0 +1,265 @@
/**
* 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
};

View file

@ -0,0 +1,64 @@
/**
* Admin Routes
* Moderation queue and system management
*/
const express = require('express');
const router = express.Router();
const adminController = require('../controllers/admin.controller');
const { authenticateToken, requireRole } = require('../middleware/auth.middleware');
const { validateRequired, validateObjectId } = require('../middleware/validation.middleware');
const { asyncHandler } = require('../middleware/error.middleware');
/**
* All admin routes require authentication
*/
router.use(authenticateToken);
/**
* Moderation Queue
*/
// GET /api/admin/moderation - List moderation queue items
router.get('/moderation',
requireRole('admin', 'moderator'),
asyncHandler(adminController.getModerationQueue)
);
// GET /api/admin/moderation/:id - Get single moderation item
router.get('/moderation/:id',
requireRole('admin', 'moderator'),
validateObjectId('id'),
asyncHandler(adminController.getModerationItem)
);
// POST /api/admin/moderation/:id/review - Review item (approve/reject/escalate)
router.post('/moderation/:id/review',
requireRole('admin', 'moderator'),
validateObjectId('id'),
validateRequired(['action']),
asyncHandler(adminController.reviewModerationItem)
);
/**
* System Statistics
*/
// GET /api/admin/stats - Get system statistics
router.get('/stats',
requireRole('admin', 'moderator'),
asyncHandler(adminController.getSystemStats)
);
/**
* Activity Log
*/
// GET /api/admin/activity - Get recent activity log
router.get('/activity',
requireRole('admin'),
asyncHandler(adminController.getActivityLog)
);
module.exports = router;

41
src/routes/auth.routes.js Normal file
View file

@ -0,0 +1,41 @@
/**
* Authentication Routes
*/
const express = require('express');
const router = express.Router();
const authController = require('../controllers/auth.controller');
const { authenticateToken } = require('../middleware/auth.middleware');
const { validateEmail, validateRequired } = require('../middleware/validation.middleware');
const { asyncHandler } = require('../middleware/error.middleware');
/**
* POST /api/auth/login
* Login with email and password
*/
router.post('/login',
validateRequired(['email', 'password']),
validateEmail,
asyncHandler(authController.login)
);
/**
* GET /api/auth/me
* Get current authenticated user
*/
router.get('/me',
authenticateToken,
asyncHandler(authController.getCurrentUser)
);
/**
* POST /api/auth/logout
* Logout (logs the event, client removes token)
*/
router.post('/logout',
authenticateToken,
asyncHandler(authController.logout)
);
module.exports = router;

80
src/routes/blog.routes.js Normal file
View file

@ -0,0 +1,80 @@
/**
* Blog Routes
* AI-curated blog endpoints
*/
const express = require('express');
const router = express.Router();
const blogController = require('../controllers/blog.controller');
const { authenticateToken, requireRole } = require('../middleware/auth.middleware');
const { validateRequired, validateObjectId, validateSlug } = require('../middleware/validation.middleware');
const { asyncHandler } = require('../middleware/error.middleware');
/**
* Public routes
*/
// GET /api/blog - List published posts
router.get('/',
asyncHandler(blogController.listPublishedPosts)
);
// GET /api/blog/:slug - Get published post by slug
router.get('/:slug',
asyncHandler(blogController.getPublishedPost)
);
/**
* Admin routes
*/
// GET /api/blog/admin/posts?status=draft
router.get('/admin/posts',
authenticateToken,
requireRole('admin', 'moderator'),
asyncHandler(blogController.listPostsByStatus)
);
// GET /api/blog/admin/:id - Get any post by ID
router.get('/admin/:id',
authenticateToken,
requireRole('admin', 'moderator'),
validateObjectId('id'),
asyncHandler(blogController.getPostById)
);
// POST /api/blog - Create new post
router.post('/',
authenticateToken,
requireRole('admin'),
validateRequired(['title', 'slug', 'content']),
validateSlug,
asyncHandler(blogController.createPost)
);
// PUT /api/blog/:id - Update post
router.put('/:id',
authenticateToken,
requireRole('admin'),
validateObjectId('id'),
asyncHandler(blogController.updatePost)
);
// POST /api/blog/:id/publish - Publish post
router.post('/:id/publish',
authenticateToken,
requireRole('admin'),
validateObjectId('id'),
asyncHandler(blogController.publishPost)
);
// DELETE /api/blog/:id - Delete post
router.delete('/:id',
authenticateToken,
requireRole('admin'),
validateObjectId('id'),
asyncHandler(blogController.deletePost)
);
module.exports = router;

View file

@ -0,0 +1,62 @@
/**
* 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
router.get('/',
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)
);
module.exports = router;

64
src/routes/index.js Normal file
View file

@ -0,0 +1,64 @@
/**
* Routes Index
* Central routing configuration
*/
const express = require('express');
const router = express.Router();
// Import route modules
const authRoutes = require('./auth.routes');
const documentsRoutes = require('./documents.routes');
const blogRoutes = require('./blog.routes');
const adminRoutes = require('./admin.routes');
// Mount routes
router.use('/auth', authRoutes);
router.use('/documents', documentsRoutes);
router.use('/blog', blogRoutes);
router.use('/admin', adminRoutes);
// API root endpoint
router.get('/', (req, res) => {
res.json({
name: 'Tractatus AI Safety Framework API',
version: '1.0.0',
status: 'operational',
endpoints: {
auth: {
login: 'POST /api/auth/login',
me: 'GET /api/auth/me',
logout: 'POST /api/auth/logout'
},
documents: {
list: 'GET /api/documents',
get: 'GET /api/documents/:identifier',
search: 'GET /api/documents/search?q=query',
create: 'POST /api/documents (admin)',
update: 'PUT /api/documents/:id (admin)',
delete: 'DELETE /api/documents/:id (admin)'
},
blog: {
list: 'GET /api/blog',
get: 'GET /api/blog/:slug',
create: 'POST /api/blog (admin)',
update: 'PUT /api/blog/:id (admin)',
publish: 'POST /api/blog/:id/publish (admin)',
delete: 'DELETE /api/blog/:id (admin)',
admin_list: 'GET /api/blog/admin/posts?status=draft (admin)',
admin_get: 'GET /api/blog/admin/:id (admin)'
},
admin: {
moderation_queue: 'GET /api/admin/moderation',
moderation_item: 'GET /api/admin/moderation/:id',
review: 'POST /api/admin/moderation/:id/review',
stats: 'GET /api/admin/stats',
activity: 'GET /api/admin/activity'
}
},
documentation: '/api/docs',
health: '/health'
});
});
module.exports = router;

View file

@ -66,23 +66,9 @@ app.get('/health', (req, res) => {
});
});
// API routes (placeholder - will be implemented)
app.get('/api', (req, res) => {
res.json({
name: config.appName,
version: '1.0.0',
message: 'Tractatus AI Safety Framework API',
documentation: '/api/docs',
endpoints: {
documents: '/api/documents',
blog: '/api/blog',
media: '/api/media',
cases: '/api/cases',
resources: '/api/resources',
admin: '/api/admin'
}
});
});
// API routes
const apiRoutes = require('./routes/index');
app.use('/api', apiRoutes);
// Homepage (temporary)
app.get('/', (req, res) => {
@ -117,14 +103,18 @@ app.get('/', (req, res) => {
<li> Express server running (port ${config.port})</li>
<li> Database initialized (10 collections)</li>
<li> Core models implemented</li>
<li>API routes (in development)</li>
<li>API routes complete (auth, documents, blog, admin)</li>
<li> Frontend (pending)</li>
</ul>
<h2>Available Endpoints</h2>
<ul>
<li><code>GET /health</code> - Health check</li>
<li><code>GET /api</code> - API information</li>
<li><code>GET /api</code> - API documentation</li>
<li><code>POST /api/auth/login</code> - Admin login</li>
<li><code>GET /api/documents</code> - List framework documents</li>
<li><code>GET /api/blog</code> - List published blog posts</li>
<li><code>GET /api/admin/stats</code> - System statistics (auth required)</li>
</ul>
<p><em>Phase 1 Development - Not for public use</em></p>