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:
parent
067012ad24
commit
0d75492c60
12 changed files with 1782 additions and 19 deletions
342
scripts/migrate-documents.js
Executable file
342
scripts/migrate-documents.js
Executable 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
113
scripts/seed-admin.js
Executable 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;
|
||||
289
src/controllers/admin.controller.js
Normal file
289
src/controllers/admin.controller.js
Normal 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
|
||||
};
|
||||
121
src/controllers/auth.controller.js
Normal file
121
src/controllers/auth.controller.js
Normal 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
|
||||
};
|
||||
332
src/controllers/blog.controller.js
Normal file
332
src/controllers/blog.controller.js
Normal 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
|
||||
};
|
||||
265
src/controllers/documents.controller.js
Normal file
265
src/controllers/documents.controller.js
Normal 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
|
||||
};
|
||||
64
src/routes/admin.routes.js
Normal file
64
src/routes/admin.routes.js
Normal 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
41
src/routes/auth.routes.js
Normal 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
80
src/routes/blog.routes.js
Normal 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;
|
||||
62
src/routes/documents.routes.js
Normal file
62
src/routes/documents.routes.js
Normal 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
64
src/routes/index.js
Normal 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;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue