tractatus/src/controllers/research.controller.js
TheFlow ccb4bdaabf feat(api): implement research inquiry endpoint and Umami analytics
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>
2025-10-29 01:31:02 +13:00

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
};