HIGH PRIORITY: Fixes production 404 error on research inquiry form Research Inquiry API: - Add POST /api/research-inquiry endpoint for form submissions - Add admin endpoints for inquiry management (list, get, assign, respond, delete) - Create ResearchInquiry model with MongoDB integration - Add to moderation queue for human review (strategic quadrant) - Include rate limiting (5 req/min) and CSRF protection - Tested locally: endpoint responding, data saving to DB Umami Analytics (Privacy-First): - Add Docker Compose config for Umami + PostgreSQL - Create nginx reverse proxy config with SSL support - Implement privacy-first tracking script (DNT, opt-out, no cookies) - Integrate tracking across 26 public HTML pages - Exclude admin pages from tracking (privacy boundary) - Add comprehensive deployment guide (UMAMI_SETUP_GUIDE.md) - Environment variables added to .env.example Files Created (9): - src/models/ResearchInquiry.model.js - src/controllers/research.controller.js - src/routes/research.routes.js - public/js/components/umami-tracker.js - deployment-quickstart/nginx-analytics.conf - deployment-quickstart/UMAMI_SETUP_GUIDE.md - scripts/add-umami-tracking.sh - scripts/add-tracking-python.py - SESSION_SUMMARY_ANALYTICS_RESEARCH_INQUIRY.md Files Modified (29): - src/routes/index.js (research routes) - deployment-quickstart/docker-compose.yml (umami services) - deployment-quickstart/.env.example (umami config) - 26 public HTML pages (tracking script) Values Alignment: ✅ Privacy-First Design (cookie-free, DNT honored, opt-out available) ✅ Human Agency (research inquiries require human review) ✅ Data Sovereignty (self-hosted analytics, no third-party sharing) ✅ GDPR Compliance (no personal data in analytics) ✅ Transparency (open-source tools, documented setup) Testing Status: ✅ Research inquiry: Locally tested, data verified in MongoDB ⏳ Umami analytics: Pending production deployment Next Steps: 1. Deploy to production (./scripts/deploy.sh) 2. Test research form on live site 3. Deploy Umami following UMAMI_SETUP_GUIDE.md 4. Update umami-tracker.js with website ID after setup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
314 lines
7.2 KiB
JavaScript
314 lines
7.2 KiB
JavaScript
/**
|
|
* Research Inquiry Controller
|
|
* Academic research collaboration inquiry submission
|
|
*/
|
|
|
|
const ResearchInquiry = require('../models/ResearchInquiry.model');
|
|
const ModerationQueue = require('../models/ModerationQueue.model');
|
|
const logger = require('../utils/logger.util');
|
|
|
|
/**
|
|
* Submit research inquiry (public)
|
|
* POST /api/research-inquiry
|
|
*/
|
|
async function submitInquiry(req, res) {
|
|
try {
|
|
const {
|
|
name,
|
|
email,
|
|
institution,
|
|
researchQuestion,
|
|
methodology,
|
|
context,
|
|
needs,
|
|
otherNeeds,
|
|
timeline
|
|
} = req.body;
|
|
|
|
// Validate required fields
|
|
if (!name || !email || !institution) {
|
|
return res.status(400).json({
|
|
error: 'Bad Request',
|
|
message: 'Missing required contact information'
|
|
});
|
|
}
|
|
|
|
if (!researchQuestion || !methodology) {
|
|
return res.status(400).json({
|
|
error: 'Bad Request',
|
|
message: 'Missing required research information'
|
|
});
|
|
}
|
|
|
|
logger.info(`Research inquiry submitted: ${institution} - ${name}`);
|
|
|
|
// Create inquiry
|
|
const researchInquiry = await ResearchInquiry.create({
|
|
contact: {
|
|
name,
|
|
email,
|
|
institution
|
|
},
|
|
research: {
|
|
research_question: researchQuestion,
|
|
methodology,
|
|
context: context || '',
|
|
needs: Array.isArray(needs) ? needs : [],
|
|
other_needs: otherNeeds || '',
|
|
timeline: timeline || ''
|
|
},
|
|
status: 'new'
|
|
});
|
|
|
|
// Add to moderation queue for human review
|
|
await ModerationQueue.create({
|
|
type: 'RESEARCH_INQUIRY',
|
|
reference_collection: 'research_inquiries',
|
|
reference_id: researchInquiry._id,
|
|
quadrant: 'STRATEGIC', // Research collaborations are strategic
|
|
data: {
|
|
contact: {
|
|
name,
|
|
email,
|
|
institution
|
|
},
|
|
research: {
|
|
question: researchQuestion,
|
|
methodology,
|
|
needs
|
|
}
|
|
},
|
|
priority: 'high',
|
|
status: 'PENDING_APPROVAL',
|
|
requires_human_approval: true,
|
|
human_required_reason: 'Research collaborations require human review and strategic assessment'
|
|
});
|
|
|
|
logger.info(`Research inquiry created: ${researchInquiry._id}`);
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
message: 'Thank you for your research inquiry. We will review and respond within 48-72 hours.',
|
|
inquiry_id: researchInquiry._id,
|
|
governance: {
|
|
human_review: true,
|
|
note: 'All research inquiries are reviewed by humans before response'
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Submit research inquiry error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred while submitting your inquiry'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List all research inquiries (admin)
|
|
* GET /api/research-inquiry?status=new
|
|
*/
|
|
async function listInquiries(req, res) {
|
|
try {
|
|
const { status, limit = 20, skip = 0 } = req.query;
|
|
|
|
let inquiries, total;
|
|
|
|
if (status) {
|
|
inquiries = await ResearchInquiry.findByStatus(status, {
|
|
limit: parseInt(limit),
|
|
skip: parseInt(skip)
|
|
});
|
|
total = await ResearchInquiry.countByStatus(status);
|
|
} else {
|
|
inquiries = await ResearchInquiry.findAll({
|
|
limit: parseInt(limit),
|
|
skip: parseInt(skip)
|
|
});
|
|
total = await ResearchInquiry.countAll();
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
status: status || 'all',
|
|
inquiries,
|
|
pagination: {
|
|
total,
|
|
limit: parseInt(limit),
|
|
skip: parseInt(skip),
|
|
hasMore: parseInt(skip) + inquiries.length < total
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('List research inquiries error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get research inquiry by ID (admin)
|
|
* GET /api/research-inquiry/:id
|
|
*/
|
|
async function getInquiry(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const inquiry = await ResearchInquiry.findById(id);
|
|
|
|
if (!inquiry) {
|
|
return res.status(404).json({
|
|
error: 'Not Found',
|
|
message: 'Research inquiry not found'
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
inquiry
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Get research inquiry error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assign inquiry to user (admin)
|
|
* POST /api/research-inquiry/: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 ResearchInquiry.assign(id, userId);
|
|
|
|
if (!success) {
|
|
return res.status(404).json({
|
|
error: 'Not Found',
|
|
message: 'Research inquiry not found'
|
|
});
|
|
}
|
|
|
|
logger.info(`Research inquiry ${id} assigned to ${userId} by ${req.user.email}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Inquiry assigned successfully'
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Assign research inquiry error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Respond to inquiry (admin)
|
|
* POST /api/research-inquiry/: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 ResearchInquiry.findById(id);
|
|
|
|
if (!inquiry) {
|
|
return res.status(404).json({
|
|
error: 'Not Found',
|
|
message: 'Research inquiry not found'
|
|
});
|
|
}
|
|
|
|
const success = await ResearchInquiry.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(`Research inquiry ${id} responded to by ${req.user.email}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Response recorded successfully',
|
|
note: 'Remember to send actual email to researcher separately'
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Respond to research inquiry error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete research inquiry (admin)
|
|
* DELETE /api/research-inquiry/:id
|
|
*/
|
|
async function deleteInquiry(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const success = await ResearchInquiry.delete(id);
|
|
|
|
if (!success) {
|
|
return res.status(404).json({
|
|
error: 'Not Found',
|
|
message: 'Research inquiry not found'
|
|
});
|
|
}
|
|
|
|
logger.info(`Research inquiry deleted: ${id} by ${req.user.email}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Inquiry deleted successfully'
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Delete research inquiry error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
submitInquiry,
|
|
listInquiries,
|
|
getInquiry,
|
|
assignInquiry,
|
|
respondToInquiry,
|
|
deleteInquiry
|
|
};
|