diff --git a/public/case-submission.html b/public/case-submission.html new file mode 100644 index 00000000..140549f8 --- /dev/null +++ b/public/case-submission.html @@ -0,0 +1,326 @@ + + + + + + Submit Case Study | Tractatus AI Safety + + + + + + + + + +
+ + +
+

Submit Case Study

+

+ Share real-world examples of AI safety failures that could have been prevented by the Tractatus Framework. +

+
+

What makes a good case study?

+
    +
  • Documented failure: Real incident with evidence (not hypothetical)
  • +
  • Clear failure mode: Specific way the AI system went wrong
  • +
  • Tractatus relevance: Shows how framework boundaries could have helped
  • +
  • Public interest: Contributes to AI safety knowledge
  • +
+
+
+ + +
+
+ + +
+
+ + +

Your Information

+ +
+ + +
+ +
+ + +

We'll only use this to follow up on your submission

+
+ +
+ + +
+ +
+
+ + +
+

Leave unchecked to remain anonymous

+
+ + +

Case Study Details

+ +
+ + +

Brief, descriptive title (e.g., "ChatGPT Port 27027 Failure")

+
+ +
+ + +

What happened? Provide context, timeline, and outcomes

+
+ +
+ + +

+ How did the AI system fail? What specific behavior went wrong? +

+
+ +
+ + +

+ Which Tractatus boundaries could have prevented this failure? (e.g., Section 12.1 Values, CrossReferenceValidator, etc.) +

+
+ +
+ + +

+ Links to documentation, screenshots, articles, or other evidence (one per line) +

+
+ + +
+ +

+ We review all submissions. High-quality case studies are published with attribution (if consented). +

+
+ +
+
+ + +
+

+ Your submission is handled according to our + privacy principles. + All case studies undergo human review before publication. +

+
+ +
+ + + + + + + + diff --git a/public/media-inquiry.html b/public/media-inquiry.html new file mode 100644 index 00000000..a9b6622d --- /dev/null +++ b/public/media-inquiry.html @@ -0,0 +1,273 @@ + + + + + + Media Inquiry | Tractatus AI Safety + + + + + + + + + +
+ + +
+

Media Inquiry

+

+ Press and media inquiries about the Tractatus Framework. We review all inquiries and respond promptly. +

+
+ + +
+
+ + +
+
+ + +

Contact Information

+ +
+ + +
+ +
+ + +
+ +
+ + +

Publication, website, podcast, or organization you represent

+
+ +
+ + +
+ + +

Inquiry Details

+ +
+ + +
+ +
+ + +
+ +
+ + +

When do you need a response by?

+
+ + +
+ +

+ We review all media inquiries and typically respond within 24-48 hours. +

+
+ +
+
+ + +
+

+ Your contact information is handled according to our + privacy principles. + We never share media inquiries with third parties. +

+
+ +
+ + + + + + + + diff --git a/src/controllers/cases.controller.js b/src/controllers/cases.controller.js new file mode 100644 index 00000000..f346787a --- /dev/null +++ b/src/controllers/cases.controller.js @@ -0,0 +1,380 @@ +/** + * Case Study Controller + * Community case study submissions with AI review + */ + +const CaseSubmission = require('../models/CaseSubmission.model'); +const ModerationQueue = require('../models/ModerationQueue.model'); +const GovernanceLog = require('../models/GovernanceLog.model'); +const BoundaryEnforcer = require('../services/BoundaryEnforcer.service'); +const logger = require('../utils/logger.util'); + +/** + * Submit case study (public) + * POST /api/cases/submit + * + * Phase 1: Manual review (no AI) + * Phase 2: Add AI categorization with claudeAPI.reviewCaseStudy() + */ +async function submitCase(req, res) { + try { + const { submitter, case_study } = req.body; + + // Validate required fields + if (!submitter?.name || !submitter?.email) { + return res.status(400).json({ + error: 'Bad Request', + message: 'Missing required submitter information' + }); + } + + if (!case_study?.title || !case_study?.description || !case_study?.failure_mode) { + return res.status(400).json({ + error: 'Bad Request', + message: 'Missing required case study information' + }); + } + + logger.info(`Case study submitted: ${case_study.title} by ${submitter.name}`); + + // Create submission (Phase 1: no AI review yet) + const submission = await CaseSubmission.create({ + submitter, + case_study, + ai_review: { + relevance_score: 0.5, // Default, will be AI-assessed in Phase 2 + completeness_score: 0.5, + recommended_category: 'uncategorized' + }, + moderation: { + status: 'pending' + } + }); + + // Add to moderation queue for human review + await ModerationQueue.create({ + type: 'CASE_SUBMISSION', + reference_collection: 'case_submissions', + reference_id: submission._id, + quadrant: 'OPERATIONAL', + data: { + submitter, + case_study + }, + priority: 'medium', + status: 'PENDING_APPROVAL', + requires_human_approval: true, + human_required_reason: 'All case submissions require human review and approval' + }); + + logger.info(`Case submission created: ${submission._id}`); + + res.status(201).json({ + success: true, + message: 'Thank you for your submission. We will review it shortly.', + submission_id: submission._id, + governance: { + human_review: true, + note: 'All case studies are reviewed by humans before publication' + } + }); + + } catch (error) { + logger.error('Submit case error:', error); + res.status(500).json({ + error: 'Internal Server Error', + message: 'An error occurred while submitting your case study' + }); + } +} + +/** + * List all case submissions (admin) + * GET /api/cases/submissions?status=pending + */ +async function listSubmissions(req, res) { + try { + const { status = 'pending', limit = 20, skip = 0 } = req.query; + + const submissions = await CaseSubmission.findByStatus(status, { + limit: parseInt(limit), + skip: parseInt(skip) + }); + + const total = await CaseSubmission.countByStatus(status); + + res.json({ + success: true, + status, + submissions, + pagination: { + total, + limit: parseInt(limit), + skip: parseInt(skip), + hasMore: parseInt(skip) + submissions.length < total + } + }); + + } catch (error) { + logger.error('List submissions error:', error); + res.status(500).json({ + error: 'Internal Server Error', + message: 'An error occurred' + }); + } +} + +/** + * List high-relevance pending submissions (admin) + * GET /api/cases/submissions/high-relevance + */ +async function listHighRelevance(req, res) { + try { + const { limit = 10 } = req.query; + + const submissions = await CaseSubmission.findHighRelevance({ + limit: parseInt(limit) + }); + + res.json({ + success: true, + count: submissions.length, + submissions + }); + + } catch (error) { + logger.error('List high relevance error:', error); + res.status(500).json({ + error: 'Internal Server Error', + message: 'An error occurred' + }); + } +} + +/** + * Get case submission by ID (admin) + * GET /api/cases/submissions/:id + */ +async function getSubmission(req, res) { + try { + const { id } = req.params; + + const submission = await CaseSubmission.findById(id); + + if (!submission) { + return res.status(404).json({ + error: 'Not Found', + message: 'Case submission not found' + }); + } + + res.json({ + success: true, + submission + }); + + } catch (error) { + logger.error('Get submission error:', error); + res.status(500).json({ + error: 'Internal Server Error', + message: 'An error occurred' + }); + } +} + +/** + * Approve case submission (admin) + * POST /api/cases/submissions/:id/approve + */ +async function approveSubmission(req, res) { + try { + const { id } = req.params; + const { notes } = req.body; + + const submission = await CaseSubmission.findById(id); + + if (!submission) { + return res.status(404).json({ + error: 'Not Found', + message: 'Case submission not found' + }); + } + + if (submission.moderation.status === 'approved') { + return res.status(400).json({ + error: 'Bad Request', + message: 'Submission is already approved' + }); + } + + const success = await CaseSubmission.approve(id, req.user._id, notes || ''); + + if (!success) { + return res.status(500).json({ + error: 'Internal Server Error', + message: 'Failed to approve submission' + }); + } + + logger.info(`Case submission approved: ${id} by ${req.user.email}`); + + res.json({ + success: true, + message: 'Case submission approved successfully', + note: 'You can now publish this as a case study document' + }); + + } catch (error) { + logger.error('Approve submission error:', error); + res.status(500).json({ + error: 'Internal Server Error', + message: 'An error occurred' + }); + } +} + +/** + * Reject case submission (admin) + * POST /api/cases/submissions/:id/reject + */ +async function rejectSubmission(req, res) { + try { + const { id } = req.params; + const { reason } = req.body; + + if (!reason) { + return res.status(400).json({ + error: 'Bad Request', + message: 'Rejection reason is required' + }); + } + + const submission = await CaseSubmission.findById(id); + + if (!submission) { + return res.status(404).json({ + error: 'Not Found', + message: 'Case submission not found' + }); + } + + const success = await CaseSubmission.reject(id, req.user._id, reason); + + if (!success) { + return res.status(500).json({ + error: 'Internal Server Error', + message: 'Failed to reject submission' + }); + } + + logger.info(`Case submission rejected: ${id} by ${req.user.email}`); + + res.json({ + success: true, + message: 'Case submission rejected', + note: 'Consider notifying the submitter with feedback' + }); + + } catch (error) { + logger.error('Reject submission error:', error); + res.status(500).json({ + error: 'Internal Server Error', + message: 'An error occurred' + }); + } +} + +/** + * Request more information (admin) + * POST /api/cases/submissions/:id/request-info + */ +async function requestMoreInfo(req, res) { + try { + const { id } = req.params; + const { requested_info } = req.body; + + if (!requested_info) { + return res.status(400).json({ + error: 'Bad Request', + message: 'Requested information must be specified' + }); + } + + const submission = await CaseSubmission.findById(id); + + if (!submission) { + return res.status(404).json({ + error: 'Not Found', + message: 'Case submission not found' + }); + } + + const success = await CaseSubmission.requestInfo(id, req.user._id, requested_info); + + if (!success) { + return res.status(500).json({ + error: 'Internal Server Error', + message: 'Failed to update submission' + }); + } + + logger.info(`More info requested for case ${id} by ${req.user.email}`); + + res.json({ + success: true, + message: 'Information request recorded', + note: 'Remember to contact submitter separately to request additional information' + }); + + } catch (error) { + logger.error('Request info error:', error); + res.status(500).json({ + error: 'Internal Server Error', + message: 'An error occurred' + }); + } +} + +/** + * Delete case submission (admin) + * DELETE /api/cases/submissions/:id + */ +async function deleteSubmission(req, res) { + try { + const { id } = req.params; + + const success = await CaseSubmission.delete(id); + + if (!success) { + return res.status(404).json({ + error: 'Not Found', + message: 'Case submission not found' + }); + } + + logger.info(`Case submission deleted: ${id} by ${req.user.email}`); + + res.json({ + success: true, + message: 'Case submission deleted successfully' + }); + + } catch (error) { + logger.error('Delete submission error:', error); + res.status(500).json({ + error: 'Internal Server Error', + message: 'An error occurred' + }); + } +} + +module.exports = { + submitCase, + listSubmissions, + listHighRelevance, + getSubmission, + approveSubmission, + rejectSubmission, + requestMoreInfo, + deleteSubmission +}; diff --git a/src/controllers/media.controller.js b/src/controllers/media.controller.js new file mode 100644 index 00000000..dc6e079e --- /dev/null +++ b/src/controllers/media.controller.js @@ -0,0 +1,314 @@ +/** + * Media Inquiry Controller + * Press/media inquiry submission and AI triage + */ + +const MediaInquiry = require('../models/MediaInquiry.model'); +const ModerationQueue = require('../models/ModerationQueue.model'); +const GovernanceLog = require('../models/GovernanceLog.model'); +const BoundaryEnforcer = require('../services/BoundaryEnforcer.service'); +const logger = require('../utils/logger.util'); + +/** + * Submit media inquiry (public) + * POST /api/media/inquiries + * + * Phase 1: Manual triage (no AI) + * Phase 2: Add AI triage with claudeAPI.triageMediaInquiry() + */ +async function submitInquiry(req, res) { + try { + const { contact, inquiry } = req.body; + + // Validate required fields + if (!contact?.name || !contact?.email || !contact?.outlet) { + return res.status(400).json({ + error: 'Bad Request', + message: 'Missing required contact information' + }); + } + + if (!inquiry?.subject || !inquiry?.message) { + return res.status(400).json({ + error: 'Bad Request', + message: 'Missing required inquiry information' + }); + } + + logger.info(`Media inquiry submitted: ${contact.outlet} - ${inquiry.subject}`); + + // Create inquiry (Phase 1: no AI triage yet) + const mediaInquiry = await MediaInquiry.create({ + contact, + inquiry, + status: 'new', + ai_triage: { + urgency: 'medium', // Default, will be AI-assessed in Phase 2 + topic_sensitivity: 'standard', + involves_values: false + } + }); + + // Add to moderation queue for human review + await ModerationQueue.create({ + type: 'MEDIA_INQUIRY', + reference_collection: 'media_inquiries', + reference_id: mediaInquiry._id, + quadrant: 'OPERATIONAL', + data: { + contact, + inquiry + }, + priority: 'medium', + status: 'PENDING_APPROVAL', + requires_human_approval: true, + human_required_reason: 'All media inquiries require human review and response' + }); + + logger.info(`Media inquiry created: ${mediaInquiry._id}`); + + res.status(201).json({ + success: true, + message: 'Thank you for your inquiry. We will review and respond shortly.', + inquiry_id: mediaInquiry._id, + governance: { + human_review: true, + note: 'All media inquiries are reviewed by humans before response' + } + }); + + } catch (error) { + logger.error('Submit inquiry error:', error); + res.status(500).json({ + error: 'Internal Server Error', + message: 'An error occurred while submitting your inquiry' + }); + } +} + +/** + * List all media inquiries (admin) + * GET /api/media/inquiries?status=new + */ +async function listInquiries(req, res) { + try { + const { status = 'new', limit = 20, skip = 0 } = req.query; + + const inquiries = await MediaInquiry.findByStatus(status, { + limit: parseInt(limit), + skip: parseInt(skip) + }); + + const total = await MediaInquiry.countByStatus(status); + + res.json({ + success: true, + status, + inquiries, + pagination: { + total, + limit: parseInt(limit), + skip: parseInt(skip), + hasMore: parseInt(skip) + inquiries.length < total + } + }); + + } catch (error) { + logger.error('List inquiries error:', error); + res.status(500).json({ + error: 'Internal Server Error', + message: 'An error occurred' + }); + } +} + +/** + * List urgent media inquiries (admin) + * GET /api/media/inquiries/urgent + */ +async function listUrgentInquiries(req, res) { + try { + const { limit = 10 } = req.query; + + const inquiries = await MediaInquiry.findUrgent({ + limit: parseInt(limit) + }); + + res.json({ + success: true, + count: inquiries.length, + inquiries + }); + + } catch (error) { + logger.error('List urgent inquiries error:', error); + res.status(500).json({ + error: 'Internal Server Error', + message: 'An error occurred' + }); + } +} + +/** + * Get media inquiry by ID (admin) + * GET /api/media/inquiries/:id + */ +async function getInquiry(req, res) { + try { + const { id } = req.params; + + const inquiry = await MediaInquiry.findById(id); + + if (!inquiry) { + return res.status(404).json({ + error: 'Not Found', + message: 'Media inquiry not found' + }); + } + + res.json({ + success: true, + inquiry + }); + + } catch (error) { + logger.error('Get inquiry error:', error); + res.status(500).json({ + error: 'Internal Server Error', + message: 'An error occurred' + }); + } +} + +/** + * Assign inquiry to user (admin) + * POST /api/media/inquiries/:id/assign + */ +async function assignInquiry(req, res) { + try { + const { id } = req.params; + const { user_id } = req.body; + + const userId = user_id || req.user._id; + + const success = await MediaInquiry.assign(id, userId); + + if (!success) { + return res.status(404).json({ + error: 'Not Found', + message: 'Media inquiry not found' + }); + } + + logger.info(`Media inquiry ${id} assigned to ${userId} by ${req.user.email}`); + + res.json({ + success: true, + message: 'Inquiry assigned successfully' + }); + + } catch (error) { + logger.error('Assign inquiry error:', error); + res.status(500).json({ + error: 'Internal Server Error', + message: 'An error occurred' + }); + } +} + +/** + * Respond to inquiry (admin) + * POST /api/media/inquiries/:id/respond + */ +async function respondToInquiry(req, res) { + try { + const { id } = req.params; + const { content } = req.body; + + if (!content) { + return res.status(400).json({ + error: 'Bad Request', + message: 'Response content is required' + }); + } + + const inquiry = await MediaInquiry.findById(id); + + if (!inquiry) { + return res.status(404).json({ + error: 'Not Found', + message: 'Media inquiry not found' + }); + } + + const success = await MediaInquiry.respond(id, { + content, + responder: req.user.email + }); + + if (!success) { + return res.status(500).json({ + error: 'Internal Server Error', + message: 'Failed to update inquiry' + }); + } + + logger.info(`Media inquiry ${id} responded to by ${req.user.email}`); + + res.json({ + success: true, + message: 'Response recorded successfully', + note: 'Remember to send actual email to media contact separately' + }); + + } catch (error) { + logger.error('Respond to inquiry error:', error); + res.status(500).json({ + error: 'Internal Server Error', + message: 'An error occurred' + }); + } +} + +/** + * Delete media inquiry (admin) + * DELETE /api/media/inquiries/:id + */ +async function deleteInquiry(req, res) { + try { + const { id } = req.params; + + const success = await MediaInquiry.delete(id); + + if (!success) { + return res.status(404).json({ + error: 'Not Found', + message: 'Media inquiry not found' + }); + } + + logger.info(`Media inquiry deleted: ${id} by ${req.user.email}`); + + res.json({ + success: true, + message: 'Inquiry deleted successfully' + }); + + } catch (error) { + logger.error('Delete inquiry error:', error); + res.status(500).json({ + error: 'Internal Server Error', + message: 'An error occurred' + }); + } +} + +module.exports = { + submitInquiry, + listInquiries, + listUrgentInquiries, + getInquiry, + assignInquiry, + respondToInquiry, + deleteInquiry +}; diff --git a/src/middleware/validation.middleware.js b/src/middleware/validation.middleware.js index f896a1a0..c5ad2384 100644 --- a/src/middleware/validation.middleware.js +++ b/src/middleware/validation.middleware.js @@ -8,32 +8,61 @@ const sanitizeHtml = require('sanitize-html'); /** * Validate email + * Supports nested fields: validateEmail('contact.email') */ -function validateEmail(req, res, next) { - const { email } = req.body; +function validateEmail(fieldPath = 'email') { + return (req, res, next) => { + // Get value from nested path (e.g., 'contact.email') + const getValue = (obj, path) => { + return path.split('.').reduce((current, key) => current?.[key], obj); + }; - if (!email || !validator.isEmail(email)) { - return res.status(400).json({ - error: 'Validation failed', - message: 'Valid email address is required' - }); - } + // Set value at nested path + const setValue = (obj, path, value) => { + const keys = path.split('.'); + const lastKey = keys.pop(); + const target = keys.reduce((current, key) => { + if (!current[key]) current[key] = {}; + return current[key]; + }, obj); + target[lastKey] = value; + }; - // Normalize email - req.body.email = validator.normalizeEmail(email); + const email = getValue(req.body, fieldPath); - next(); + if (!email || !validator.isEmail(email)) { + return res.status(400).json({ + error: 'Validation failed', + message: `Valid email address is required for ${fieldPath}` + }); + } + + // Normalize email + const normalized = validator.normalizeEmail(email); + setValue(req.body, fieldPath, normalized); + + next(); + }; } /** * Validate required fields + * Supports nested fields: validateRequired(['contact.name', 'contact.email']) */ function validateRequired(fields) { return (req, res, next) => { const missing = []; + // Get value from nested path (e.g., 'contact.email') + const getValue = (obj, path) => { + return path.split('.').reduce((current, key) => current?.[key], obj); + }; + for (const field of fields) { - if (!req.body[field] || req.body[field].trim() === '') { + const value = getValue(req.body, field); + + if (value === undefined || value === null || + (typeof value === 'string' && value.trim() === '')) { missing.push(field); } } diff --git a/src/routes/auth.routes.js b/src/routes/auth.routes.js index dc1d07f5..bd23cd3d 100644 --- a/src/routes/auth.routes.js +++ b/src/routes/auth.routes.js @@ -16,7 +16,7 @@ const { asyncHandler } = require('../middleware/error.middleware'); */ router.post('/login', validateRequired(['email', 'password']), - validateEmail, + validateEmail('email'), asyncHandler(authController.login) ); diff --git a/src/routes/cases.routes.js b/src/routes/cases.routes.js new file mode 100644 index 00000000..b5bcca37 --- /dev/null +++ b/src/routes/cases.routes.js @@ -0,0 +1,91 @@ +/** + * Case Study Routes + * Community case study submission endpoints + */ + +const express = require('express'); +const router = express.Router(); + +const casesController = require('../controllers/cases.controller'); +const { authenticateToken, requireRole } = require('../middleware/auth.middleware'); +const { validateRequired, validateEmail, validateObjectId } = require('../middleware/validation.middleware'); +const { asyncHandler } = require('../middleware/error.middleware'); + +/** + * Public routes + */ + +// POST /api/cases/submit - Submit case study (public) +router.post('/submit', + validateRequired([ + 'submitter.name', + 'submitter.email', + 'case_study.title', + 'case_study.description', + 'case_study.failure_mode' + ]), + validateEmail('submitter.email'), + asyncHandler(casesController.submitCase) +); + +/** + * Admin routes + */ + +// GET /api/cases/submissions - List all submissions (admin) +router.get('/submissions', + authenticateToken, + requireRole('admin', 'moderator'), + asyncHandler(casesController.listSubmissions) +); + +// GET /api/cases/submissions/high-relevance - List high-relevance pending (admin) +router.get('/submissions/high-relevance', + authenticateToken, + requireRole('admin', 'moderator'), + asyncHandler(casesController.listHighRelevance) +); + +// GET /api/cases/submissions/:id - Get submission by ID (admin) +router.get('/submissions/:id', + authenticateToken, + requireRole('admin', 'moderator'), + validateObjectId('id'), + asyncHandler(casesController.getSubmission) +); + +// POST /api/cases/submissions/:id/approve - Approve submission (admin) +router.post('/submissions/:id/approve', + authenticateToken, + requireRole('admin'), + validateObjectId('id'), + asyncHandler(casesController.approveSubmission) +); + +// POST /api/cases/submissions/:id/reject - Reject submission (admin) +router.post('/submissions/:id/reject', + authenticateToken, + requireRole('admin'), + validateObjectId('id'), + validateRequired(['reason']), + asyncHandler(casesController.rejectSubmission) +); + +// POST /api/cases/submissions/:id/request-info - Request more information (admin) +router.post('/submissions/:id/request-info', + authenticateToken, + requireRole('admin', 'moderator'), + validateObjectId('id'), + validateRequired(['requested_info']), + asyncHandler(casesController.requestMoreInfo) +); + +// DELETE /api/cases/submissions/:id - Delete submission (admin) +router.delete('/submissions/:id', + authenticateToken, + requireRole('admin'), + validateObjectId('id'), + asyncHandler(casesController.deleteSubmission) +); + +module.exports = router; diff --git a/src/routes/index.js b/src/routes/index.js index 736f1233..1760de9f 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -10,6 +10,8 @@ const router = express.Router(); const authRoutes = require('./auth.routes'); const documentsRoutes = require('./documents.routes'); const blogRoutes = require('./blog.routes'); +const mediaRoutes = require('./media.routes'); +const casesRoutes = require('./cases.routes'); const adminRoutes = require('./admin.routes'); const governanceRoutes = require('./governance.routes'); @@ -17,6 +19,8 @@ const governanceRoutes = require('./governance.routes'); router.use('/auth', authRoutes); router.use('/documents', documentsRoutes); router.use('/blog', blogRoutes); +router.use('/media', mediaRoutes); +router.use('/cases', casesRoutes); router.use('/admin', adminRoutes); router.use('/governance', governanceRoutes); @@ -48,7 +52,27 @@ router.get('/', (req, res) => { 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_get: 'GET /api/blog/admin/:id (admin)', + suggest_topics: 'POST /api/blog/suggest-topics (admin)' + }, + media: { + submit: 'POST /api/media/inquiries', + list: 'GET /api/media/inquiries (admin)', + urgent: 'GET /api/media/inquiries/urgent (admin)', + get: 'GET /api/media/inquiries/:id (admin)', + assign: 'POST /api/media/inquiries/:id/assign (admin)', + respond: 'POST /api/media/inquiries/:id/respond (admin)', + delete: 'DELETE /api/media/inquiries/:id (admin)' + }, + cases: { + submit: 'POST /api/cases/submit', + list: 'GET /api/cases/submissions (admin)', + high_relevance: 'GET /api/cases/submissions/high-relevance (admin)', + get: 'GET /api/cases/submissions/:id (admin)', + approve: 'POST /api/cases/submissions/:id/approve (admin)', + reject: 'POST /api/cases/submissions/:id/reject (admin)', + request_info: 'POST /api/cases/submissions/:id/request-info (admin)', + delete: 'DELETE /api/cases/submissions/:id (admin)' }, admin: { moderation_queue: 'GET /api/admin/moderation', diff --git a/src/routes/media.routes.js b/src/routes/media.routes.js new file mode 100644 index 00000000..12608ee8 --- /dev/null +++ b/src/routes/media.routes.js @@ -0,0 +1,76 @@ +/** + * Media Inquiry Routes + * Press/media inquiry submission and triage endpoints + */ + +const express = require('express'); +const router = express.Router(); + +const mediaController = require('../controllers/media.controller'); +const { authenticateToken, requireRole } = require('../middleware/auth.middleware'); +const { validateRequired, validateEmail, validateObjectId } = require('../middleware/validation.middleware'); +const { asyncHandler } = require('../middleware/error.middleware'); + +/** + * Public routes + */ + +// POST /api/media/inquiries - Submit media inquiry (public) +router.post('/inquiries', + validateRequired(['contact.name', 'contact.email', 'contact.outlet', 'inquiry.subject', 'inquiry.message']), + validateEmail('contact.email'), + asyncHandler(mediaController.submitInquiry) +); + +/** + * Admin routes + */ + +// GET /api/media/inquiries - List all inquiries (admin) +router.get('/inquiries', + authenticateToken, + requireRole('admin', 'moderator'), + asyncHandler(mediaController.listInquiries) +); + +// GET /api/media/inquiries/urgent - List high urgency inquiries (admin) +router.get('/inquiries/urgent', + authenticateToken, + requireRole('admin', 'moderator'), + asyncHandler(mediaController.listUrgentInquiries) +); + +// GET /api/media/inquiries/:id - Get inquiry by ID (admin) +router.get('/inquiries/:id', + authenticateToken, + requireRole('admin', 'moderator'), + validateObjectId('id'), + asyncHandler(mediaController.getInquiry) +); + +// POST /api/media/inquiries/:id/assign - Assign inquiry to user (admin) +router.post('/inquiries/:id/assign', + authenticateToken, + requireRole('admin'), + validateObjectId('id'), + asyncHandler(mediaController.assignInquiry) +); + +// POST /api/media/inquiries/:id/respond - Mark as responded (admin) +router.post('/inquiries/:id/respond', + authenticateToken, + requireRole('admin', 'moderator'), + validateObjectId('id'), + validateRequired(['content']), + asyncHandler(mediaController.respondToInquiry) +); + +// DELETE /api/media/inquiries/:id - Delete inquiry (admin) +router.delete('/inquiries/:id', + authenticateToken, + requireRole('admin'), + validateObjectId('id'), + asyncHandler(mediaController.deleteInquiry) +); + +module.exports = router;