tractatus/src/models/ResearchInquiry.model.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

162 lines
3.9 KiB
JavaScript

/**
* ResearchInquiry Model
* Academic research collaboration inquiries
*/
const { ObjectId } = require('mongodb');
const { getCollection } = require('../utils/db.util');
class ResearchInquiry {
/**
* Create a new research inquiry
*/
static async create(data) {
const collection = await getCollection('research_inquiries');
const inquiry = {
contact: {
name: data.contact.name,
email: data.contact.email,
institution: data.contact.institution
},
research: {
research_question: data.research.research_question,
methodology: data.research.methodology,
context: data.research.context,
needs: data.research.needs || [], // Array of collaboration needs
other_needs: data.research.other_needs,
timeline: data.research.timeline
},
status: data.status || 'new', // new/reviewed/responded/closed
assigned_to: data.assigned_to,
response: {
sent_at: data.response?.sent_at,
content: data.response?.content,
responder: data.response?.responder
},
created_at: new Date()
};
const result = await collection.insertOne(inquiry);
return { ...inquiry, _id: result.insertedId };
}
/**
* Find inquiry by ID
*/
static async findById(id) {
const collection = await getCollection('research_inquiries');
return await collection.findOne({ _id: new ObjectId(id) });
}
/**
* Find inquiries by status
*/
static async findByStatus(status, options = {}) {
const collection = await getCollection('research_inquiries');
const { limit = 20, skip = 0 } = options;
return await collection
.find({ status })
.sort({ created_at: -1 })
.skip(skip)
.limit(limit)
.toArray();
}
/**
* Find all inquiries with pagination
*/
static async findAll(options = {}) {
const collection = await getCollection('research_inquiries');
const { limit = 20, skip = 0 } = options;
return await collection
.find({})
.sort({ created_at: -1 })
.skip(skip)
.limit(limit)
.toArray();
}
/**
* Update inquiry
*/
static async update(id, updates) {
const collection = await getCollection('research_inquiries');
const result = await collection.updateOne(
{ _id: new ObjectId(id) },
{ $set: updates }
);
return result.modifiedCount > 0;
}
/**
* Assign inquiry to user
*/
static async assign(id, userId) {
const collection = await getCollection('research_inquiries');
const result = await collection.updateOne(
{ _id: new ObjectId(id) },
{
$set: {
assigned_to: new ObjectId(userId),
status: 'reviewed'
}
}
);
return result.modifiedCount > 0;
}
/**
* Mark as responded
*/
static async respond(id, responseData) {
const collection = await getCollection('research_inquiries');
const result = await collection.updateOne(
{ _id: new ObjectId(id) },
{
$set: {
status: 'responded',
'response.sent_at': new Date(),
'response.content': responseData.content,
'response.responder': responseData.responder
}
}
);
return result.modifiedCount > 0;
}
/**
* Count by status
*/
static async countByStatus(status) {
const collection = await getCollection('research_inquiries');
return await collection.countDocuments({ status });
}
/**
* Count all inquiries
*/
static async countAll() {
const collection = await getCollection('research_inquiries');
return await collection.countDocuments({});
}
/**
* Delete inquiry
*/
static async delete(id) {
const collection = await getCollection('research_inquiries');
const result = await collection.deleteOne({ _id: new ObjectId(id) });
return result.deletedCount > 0;
}
}
module.exports = ResearchInquiry;