feat: implement AI-powered features (Phase 1 Core)
**Three Public Features:** - Media Inquiry System: Press/media can submit inquiries with AI triage (Phase 2) - Case Study Submissions: Community can submit real-world AI safety failures - Blog Curation: Admin-only topic suggestions with AI assistance (Phase 2) **Backend Implementation:** - Media routes/controller: /api/media/inquiries endpoints - Cases routes/controller: /api/cases/submit endpoints - Blog routes/controller: Already existed, documented - Human oversight: All submissions go to moderation queue - Tractatus boundaries: BoundaryEnforcer integration in blog controller **Frontend Forms:** - /media-inquiry.html: Public submission form for press/media - /case-submission.html: Public submission form for case studies - Full validation, error handling, success messages **Validation Middleware Updates:** - Support nested field validation (contact.email, submitter.name) - validateEmail(fieldPath) now parameterized - validateRequired() supports dot-notation paths **Phase 1 Status:** - AI triage: Manual (Phase 2 will add Claude API integration) - All submissions require human review and approval - Moderation queue operational - Admin dashboard endpoints ready **Files Added:** - public/media-inquiry.html - public/case-submission.html - src/controllers/media.controller.js - src/controllers/cases.controller.js - src/routes/media.routes.js - src/routes/cases.routes.js **Files Modified:** - src/routes/index.js (registered new routes) - src/routes/auth.routes.js (updated validateEmail call) - src/middleware/validation.middleware.js (nested field support) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
759a37fbeb
commit
682bfa2f5c
9 changed files with 1527 additions and 14 deletions
326
public/case-submission.html
Normal file
326
public/case-submission.html
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Submit Case Study | Tractatus AI Safety</title>
|
||||
<link rel="stylesheet" href="/css/tailwind.css?v=1759835330">
|
||||
<style>
|
||||
.form-group { margin-bottom: 1.5rem; }
|
||||
.form-label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.form-input, .form-textarea, .form-select {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.form-input:focus, .form-textarea:focus, .form-select:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
.form-textarea { min-height: 150px; resize: vertical; }
|
||||
.form-help { font-size: 0.875rem; color: #6b7280; margin-top: 0.25rem; }
|
||||
.required { color: #dc2626; }
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.checkbox-group input[type="checkbox"] {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
#success-message, #error-message {
|
||||
display: none;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
#success-message {
|
||||
background-color: #d1fae5;
|
||||
border: 1px solid #10b981;
|
||||
color: #065f46;
|
||||
}
|
||||
#error-message {
|
||||
background-color: #fee2e2;
|
||||
border: 1px solid #ef4444;
|
||||
color: #991b1b;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="bg-white border-b border-gray-200">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<a href="/" class="text-xl font-bold text-gray-900">Tractatus Framework</a>
|
||||
</div>
|
||||
<div class="flex items-center space-x-6">
|
||||
<a href="/researcher.html" class="text-gray-600 hover:text-gray-900">Researcher</a>
|
||||
<a href="/implementer.html" class="text-gray-600 hover:text-gray-900">Implementer</a>
|
||||
<a href="/advocate.html" class="text-gray-600 hover:text-gray-900">Advocate</a>
|
||||
<a href="/docs.html" class="text-gray-600 hover:text-gray-900">Documentation</a>
|
||||
<a href="/about.html" class="text-gray-600 hover:text-gray-900">About</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-3">Submit Case Study</h1>
|
||||
<p class="text-lg text-gray-600 mb-4">
|
||||
Share real-world examples of AI safety failures that could have been prevented by the Tractatus Framework.
|
||||
</p>
|
||||
<div class="bg-blue-50 border-l-4 border-blue-500 p-4 rounded">
|
||||
<h3 class="text-blue-900 font-semibold mb-2">What makes a good case study?</h3>
|
||||
<ul class="text-blue-800 text-sm space-y-1">
|
||||
<li>• <strong>Documented failure:</strong> Real incident with evidence (not hypothetical)</li>
|
||||
<li>• <strong>Clear failure mode:</strong> Specific way the AI system went wrong</li>
|
||||
<li>• <strong>Tractatus relevance:</strong> Shows how framework boundaries could have helped</li>
|
||||
<li>• <strong>Public interest:</strong> Contributes to AI safety knowledge</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
<div id="success-message"></div>
|
||||
<div id="error-message"></div>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-8">
|
||||
<form id="case-submission-form">
|
||||
|
||||
<!-- Submitter Information -->
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-6">Your Information</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="submitter-name" class="form-label">
|
||||
Your Name <span class="required">*</span>
|
||||
</label>
|
||||
<input type="text" id="submitter-name" name="submitter.name" class="form-input" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="submitter-email" class="form-label">
|
||||
Email Address <span class="required">*</span>
|
||||
</label>
|
||||
<input type="email" id="submitter-email" name="submitter.email" class="form-input" required>
|
||||
<p class="form-help">We'll only use this to follow up on your submission</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="submitter-organization" class="form-label">
|
||||
Organization (optional)
|
||||
</label>
|
||||
<input type="text" id="submitter-organization" name="submitter.organization" class="form-input">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" id="submitter-public" name="submitter.public">
|
||||
<label for="submitter-public" class="text-gray-700">
|
||||
I consent to my name being published with this case study
|
||||
</label>
|
||||
</div>
|
||||
<p class="form-help ml-7">Leave unchecked to remain anonymous</p>
|
||||
</div>
|
||||
|
||||
<!-- Case Study Details -->
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-6 mt-8">Case Study Details</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="case-title" class="form-label">
|
||||
Case Study Title <span class="required">*</span>
|
||||
</label>
|
||||
<input type="text" id="case-title" name="case_study.title" class="form-input" required>
|
||||
<p class="form-help">Brief, descriptive title (e.g., "ChatGPT Port 27027 Failure")</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="case-description" class="form-label">
|
||||
Detailed Description <span class="required">*</span>
|
||||
</label>
|
||||
<textarea id="case-description" name="case_study.description" class="form-textarea" required></textarea>
|
||||
<p class="form-help">What happened? Provide context, timeline, and outcomes</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="case-failure-mode" class="form-label">
|
||||
Failure Mode <span class="required">*</span>
|
||||
</label>
|
||||
<textarea id="case-failure-mode" name="case_study.failure_mode" class="form-textarea" required></textarea>
|
||||
<p class="form-help">
|
||||
How did the AI system fail? What specific behavior went wrong?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="case-tractatus" class="form-label">
|
||||
Tractatus Applicability <span class="required">*</span>
|
||||
</label>
|
||||
<textarea id="case-tractatus" name="case_study.tractatus_applicability" class="form-textarea" required></textarea>
|
||||
<p class="form-help">
|
||||
Which Tractatus boundaries could have prevented this failure? (e.g., Section 12.1 Values, CrossReferenceValidator, etc.)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="case-evidence" class="form-label">
|
||||
Evidence/Sources
|
||||
</label>
|
||||
<textarea id="case-evidence" name="case_study.evidence" class="form-textarea" style="min-height: 100px;"></textarea>
|
||||
<p class="form-help">
|
||||
Links to documentation, screenshots, articles, or other evidence (one per line)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="mt-8">
|
||||
<button type="submit" id="submit-button" class="w-full bg-blue-600 text-white px-6 py-3 rounded-lg font-semibold hover:bg-blue-700 transition">
|
||||
Submit Case Study
|
||||
</button>
|
||||
<p class="form-help mt-3 text-center">
|
||||
We review all submissions. High-quality case studies are published with attribution (if consented).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Privacy Note -->
|
||||
<div class="mt-6 text-sm text-gray-600 text-center">
|
||||
<p>
|
||||
Your submission is handled according to our
|
||||
<a href="/about/values.html" class="text-blue-600 hover:text-blue-700">privacy principles</a>.
|
||||
All case studies undergo human review before publication.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-gray-900 text-gray-300 mt-16 py-12">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div>
|
||||
<h3 class="text-white font-semibold mb-4">Tractatus Framework</h3>
|
||||
<p class="text-sm">
|
||||
AI safety through architectural constraints and human agency preservation.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-white font-semibold mb-4">Quick Links</h3>
|
||||
<ul class="space-y-2 text-sm">
|
||||
<li><a href="/docs.html" class="hover:text-white transition">Documentation</a></li>
|
||||
<li><a href="/about.html" class="hover:text-white transition">About</a></li>
|
||||
<li><a href="/about/values.html" class="hover:text-white transition">Values</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-white font-semibold mb-4">Contact</h3>
|
||||
<ul class="space-y-2 text-sm">
|
||||
<li><a href="/media-inquiry.html" class="hover:text-white transition">Media Inquiries</a></li>
|
||||
<li><a href="/case-submission.html" class="hover:text-white transition">Submit Case Study</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 pt-8 border-t border-gray-800 text-center text-sm">
|
||||
<p>© 2025 Tractatus Framework. Licensed under <a href="/LICENSE" class="text-blue-400 hover:text-blue-300 transition">Apache License 2.0</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
const form = document.getElementById('case-submission-form');
|
||||
const submitButton = document.getElementById('submit-button');
|
||||
const successMessage = document.getElementById('success-message');
|
||||
const errorMessage = document.getElementById('error-message');
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Hide previous messages
|
||||
successMessage.style.display = 'none';
|
||||
errorMessage.style.display = 'none';
|
||||
|
||||
// Disable submit button
|
||||
submitButton.disabled = true;
|
||||
submitButton.textContent = 'Submitting...';
|
||||
|
||||
// Collect form data
|
||||
const evidenceText = document.getElementById('case-evidence').value;
|
||||
const evidence = evidenceText
|
||||
? evidenceText.split('\n').filter(line => line.trim())
|
||||
: [];
|
||||
|
||||
const formData = {
|
||||
submitter: {
|
||||
name: document.getElementById('submitter-name').value,
|
||||
email: document.getElementById('submitter-email').value,
|
||||
organization: document.getElementById('submitter-organization').value || null,
|
||||
public: document.getElementById('submitter-public').checked
|
||||
},
|
||||
case_study: {
|
||||
title: document.getElementById('case-title').value,
|
||||
description: document.getElementById('case-description').value,
|
||||
failure_mode: document.getElementById('case-failure-mode').value,
|
||||
tractatus_applicability: document.getElementById('case-tractatus').value,
|
||||
evidence: evidence,
|
||||
attachments: []
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/cases/submit', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
// Success
|
||||
successMessage.textContent = data.message || 'Thank you for your submission. We will review it shortly.';
|
||||
successMessage.style.display = 'block';
|
||||
form.reset();
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
} else {
|
||||
// Error
|
||||
errorMessage.textContent = data.message || 'An error occurred. Please try again.';
|
||||
errorMessage.style.display = 'block';
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Submit error:', error);
|
||||
errorMessage.textContent = 'Network error. Please check your connection and try again.';
|
||||
errorMessage.style.display = 'block';
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
} finally {
|
||||
// Re-enable submit button
|
||||
submitButton.disabled = false;
|
||||
submitButton.textContent = 'Submit Case Study';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
273
public/media-inquiry.html
Normal file
273
public/media-inquiry.html
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Media Inquiry | Tractatus AI Safety</title>
|
||||
<link rel="stylesheet" href="/css/tailwind.css?v=1759835330">
|
||||
<style>
|
||||
.form-group { margin-bottom: 1.5rem; }
|
||||
.form-label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.form-input, .form-textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.form-input:focus, .form-textarea:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
.form-textarea { min-height: 150px; resize: vertical; }
|
||||
.form-help { font-size: 0.875rem; color: #6b7280; margin-top: 0.25rem; }
|
||||
.required { color: #dc2626; }
|
||||
#success-message, #error-message {
|
||||
display: none;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
#success-message {
|
||||
background-color: #d1fae5;
|
||||
border: 1px solid #10b981;
|
||||
color: #065f46;
|
||||
}
|
||||
#error-message {
|
||||
background-color: #fee2e2;
|
||||
border: 1px solid #ef4444;
|
||||
color: #991b1b;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="bg-white border-b border-gray-200">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<a href="/" class="text-xl font-bold text-gray-900">Tractatus Framework</a>
|
||||
</div>
|
||||
<div class="flex items-center space-x-6">
|
||||
<a href="/researcher.html" class="text-gray-600 hover:text-gray-900">Researcher</a>
|
||||
<a href="/implementer.html" class="text-gray-600 hover:text-gray-900">Implementer</a>
|
||||
<a href="/advocate.html" class="text-gray-600 hover:text-gray-900">Advocate</a>
|
||||
<a href="/docs.html" class="text-gray-600 hover:text-gray-900">Documentation</a>
|
||||
<a href="/about.html" class="text-gray-600 hover:text-gray-900">About</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-3">Media Inquiry</h1>
|
||||
<p class="text-lg text-gray-600">
|
||||
Press and media inquiries about the Tractatus Framework. We review all inquiries and respond promptly.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
<div id="success-message"></div>
|
||||
<div id="error-message"></div>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-8">
|
||||
<form id="media-inquiry-form">
|
||||
|
||||
<!-- Contact Information -->
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-6">Contact Information</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="contact-name" class="form-label">
|
||||
Your Name <span class="required">*</span>
|
||||
</label>
|
||||
<input type="text" id="contact-name" name="contact.name" class="form-input" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="contact-email" class="form-label">
|
||||
Email Address <span class="required">*</span>
|
||||
</label>
|
||||
<input type="email" id="contact-email" name="contact.email" class="form-input" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="contact-outlet" class="form-label">
|
||||
Media Outlet/Organization <span class="required">*</span>
|
||||
</label>
|
||||
<input type="text" id="contact-outlet" name="contact.outlet" class="form-input" required>
|
||||
<p class="form-help">Publication, website, podcast, or organization you represent</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="contact-phone" class="form-label">
|
||||
Phone Number (optional)
|
||||
</label>
|
||||
<input type="tel" id="contact-phone" name="contact.phone" class="form-input">
|
||||
</div>
|
||||
|
||||
<!-- Inquiry Details -->
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-6 mt-8">Inquiry Details</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="inquiry-subject" class="form-label">
|
||||
Subject <span class="required">*</span>
|
||||
</label>
|
||||
<input type="text" id="inquiry-subject" name="inquiry.subject" class="form-input" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="inquiry-message" class="form-label">
|
||||
Message <span class="required">*</span>
|
||||
</label>
|
||||
<textarea id="inquiry-message" name="inquiry.message" class="form-textarea" required></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="inquiry-deadline" class="form-label">
|
||||
Deadline (if applicable)
|
||||
</label>
|
||||
<input type="datetime-local" id="inquiry-deadline" name="inquiry.deadline" class="form-input">
|
||||
<p class="form-help">When do you need a response by?</p>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="mt-8">
|
||||
<button type="submit" id="submit-button" class="w-full bg-blue-600 text-white px-6 py-3 rounded-lg font-semibold hover:bg-blue-700 transition">
|
||||
Submit Inquiry
|
||||
</button>
|
||||
<p class="form-help mt-3 text-center">
|
||||
We review all media inquiries and typically respond within 24-48 hours.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Privacy Note -->
|
||||
<div class="mt-6 text-sm text-gray-600 text-center">
|
||||
<p>
|
||||
Your contact information is handled according to our
|
||||
<a href="/about/values.html" class="text-blue-600 hover:text-blue-700">privacy principles</a>.
|
||||
We never share media inquiries with third parties.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-gray-900 text-gray-300 mt-16 py-12">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div>
|
||||
<h3 class="text-white font-semibold mb-4">Tractatus Framework</h3>
|
||||
<p class="text-sm">
|
||||
AI safety through architectural constraints and human agency preservation.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-white font-semibold mb-4">Quick Links</h3>
|
||||
<ul class="space-y-2 text-sm">
|
||||
<li><a href="/docs.html" class="hover:text-white transition">Documentation</a></li>
|
||||
<li><a href="/about.html" class="hover:text-white transition">About</a></li>
|
||||
<li><a href="/about/values.html" class="hover:text-white transition">Values</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-white font-semibold mb-4">Contact</h3>
|
||||
<ul class="space-y-2 text-sm">
|
||||
<li><a href="/media-inquiry.html" class="hover:text-white transition">Media Inquiries</a></li>
|
||||
<li><a href="/case-submission.html" class="hover:text-white transition">Submit Case Study</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 pt-8 border-t border-gray-800 text-center text-sm">
|
||||
<p>© 2025 Tractatus Framework. Licensed under <a href="/LICENSE" class="text-blue-400 hover:text-blue-300 transition">Apache License 2.0</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
const form = document.getElementById('media-inquiry-form');
|
||||
const submitButton = document.getElementById('submit-button');
|
||||
const successMessage = document.getElementById('success-message');
|
||||
const errorMessage = document.getElementById('error-message');
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Hide previous messages
|
||||
successMessage.style.display = 'none';
|
||||
errorMessage.style.display = 'none';
|
||||
|
||||
// Disable submit button
|
||||
submitButton.disabled = true;
|
||||
submitButton.textContent = 'Submitting...';
|
||||
|
||||
// Collect form data
|
||||
const formData = {
|
||||
contact: {
|
||||
name: document.getElementById('contact-name').value,
|
||||
email: document.getElementById('contact-email').value,
|
||||
outlet: document.getElementById('contact-outlet').value,
|
||||
phone: document.getElementById('contact-phone').value || null
|
||||
},
|
||||
inquiry: {
|
||||
subject: document.getElementById('inquiry-subject').value,
|
||||
message: document.getElementById('inquiry-message').value,
|
||||
deadline: document.getElementById('inquiry-deadline').value || null,
|
||||
topic_areas: []
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/media/inquiries', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
// Success
|
||||
successMessage.textContent = data.message || 'Thank you for your inquiry. We will review and respond shortly.';
|
||||
successMessage.style.display = 'block';
|
||||
form.reset();
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
} else {
|
||||
// Error
|
||||
errorMessage.textContent = data.message || 'An error occurred. Please try again.';
|
||||
errorMessage.style.display = 'block';
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Submit error:', error);
|
||||
errorMessage.textContent = 'Network error. Please check your connection and try again.';
|
||||
errorMessage.style.display = 'block';
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
} finally {
|
||||
// Re-enable submit button
|
||||
submitButton.disabled = false;
|
||||
submitButton.textContent = 'Submit Inquiry';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
380
src/controllers/cases.controller.js
Normal file
380
src/controllers/cases.controller.js
Normal file
|
|
@ -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
|
||||
};
|
||||
314
src/controllers/media.controller.js
Normal file
314
src/controllers/media.controller.js
Normal file
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const { asyncHandler } = require('../middleware/error.middleware');
|
|||
*/
|
||||
router.post('/login',
|
||||
validateRequired(['email', 'password']),
|
||||
validateEmail,
|
||||
validateEmail('email'),
|
||||
asyncHandler(authController.login)
|
||||
);
|
||||
|
||||
|
|
|
|||
91
src/routes/cases.routes.js
Normal file
91
src/routes/cases.routes.js
Normal file
|
|
@ -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;
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
76
src/routes/media.routes.js
Normal file
76
src/routes/media.routes.js
Normal file
|
|
@ -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;
|
||||
Loading…
Add table
Reference in a new issue