feat: add MongoDB models for core collections

Models Created (7/10):
- Document.model.js: Framework docs with quadrant classification
- BlogPost.model.js: AI-curated blog with moderation
- MediaInquiry.model.js: Press/media triage workflow
- ModerationQueue.model.js: Human oversight queue with priority
- User.model.js: Admin authentication with bcrypt
- CaseSubmission.model.js: Community case studies with AI review
- Resource.model.js: Curated directory with alignment scores

Features:
- Full CRUD operations for each model
- Tractatus quadrant integration
- AI analysis fields for curation
- Human approval workflows
- Status tracking and filtering
- Security (password hashing, sanitized returns)

Deferred (Phase 2-3):
- Citation.model.js
- Translation.model.js
- KohaDonation.model.js

Status: Core models complete, ready for Express server
This commit is contained in:
TheFlow 2025-10-06 23:54:56 +13:00
parent 47818bade1
commit 78ab5754f2
8 changed files with 1305 additions and 0 deletions

View file

@ -0,0 +1,163 @@
/**
* BlogPost Model
* AI-curated blog with human oversight
*/
const { ObjectId } = require('mongodb');
const { getCollection } = require('../utils/db.util');
class BlogPost {
/**
* Create a new blog post
*/
static async create(data) {
const collection = await getCollection('blog_posts');
const post = {
title: data.title,
slug: data.slug,
author: {
type: data.author?.type || 'human', // 'human' or 'ai_curated'
name: data.author?.name || 'John Stroh',
claude_version: data.author?.claude_version
},
content: data.content,
excerpt: data.excerpt,
featured_image: data.featured_image,
status: data.status || 'draft', // draft/pending/published/archived
moderation: {
ai_analysis: data.moderation?.ai_analysis,
human_reviewer: data.moderation?.human_reviewer,
review_notes: data.moderation?.review_notes,
approved_at: data.moderation?.approved_at
},
tractatus_classification: {
quadrant: data.tractatus_classification?.quadrant || 'OPERATIONAL',
values_sensitive: data.tractatus_classification?.values_sensitive || false,
requires_strategic_review: data.tractatus_classification?.requires_strategic_review || false
},
published_at: data.published_at,
tags: data.tags || [],
view_count: 0,
engagement: {
shares: 0,
comments: 0
}
};
const result = await collection.insertOne(post);
return { ...post, _id: result.insertedId };
}
/**
* Find post by ID
*/
static async findById(id) {
const collection = await getCollection('blog_posts');
return await collection.findOne({ _id: new ObjectId(id) });
}
/**
* Find post by slug
*/
static async findBySlug(slug) {
const collection = await getCollection('blog_posts');
return await collection.findOne({ slug });
}
/**
* Find published posts
*/
static async findPublished(options = {}) {
const collection = await getCollection('blog_posts');
const { limit = 10, skip = 0, sort = { published_at: -1 } } = options;
return await collection
.find({ status: 'published' })
.sort(sort)
.skip(skip)
.limit(limit)
.toArray();
}
/**
* Find posts by status
*/
static async findByStatus(status, options = {}) {
const collection = await getCollection('blog_posts');
const { limit = 20, skip = 0 } = options;
return await collection
.find({ status })
.sort({ _id: -1 })
.skip(skip)
.limit(limit)
.toArray();
}
/**
* Update post
*/
static async update(id, updates) {
const collection = await getCollection('blog_posts');
const result = await collection.updateOne(
{ _id: new ObjectId(id) },
{ $set: updates }
);
return result.modifiedCount > 0;
}
/**
* Publish post (change status + set published_at)
*/
static async publish(id, reviewerId) {
const collection = await getCollection('blog_posts');
const result = await collection.updateOne(
{ _id: new ObjectId(id) },
{
$set: {
status: 'published',
published_at: new Date(),
'moderation.human_reviewer': reviewerId,
'moderation.approved_at': new Date()
}
}
);
return result.modifiedCount > 0;
}
/**
* Increment view count
*/
static async incrementViews(id) {
const collection = await getCollection('blog_posts');
await collection.updateOne(
{ _id: new ObjectId(id) },
{ $inc: { view_count: 1 } }
);
}
/**
* Delete post
*/
static async delete(id) {
const collection = await getCollection('blog_posts');
const result = await collection.deleteOne({ _id: new ObjectId(id) });
return result.deletedCount > 0;
}
/**
* Count posts by status
*/
static async countByStatus(status) {
const collection = await getCollection('blog_posts');
return await collection.countDocuments({ status });
}
}
module.exports = BlogPost;

View file

@ -0,0 +1,206 @@
/**
* CaseSubmission Model
* Community case study submissions
*/
const { ObjectId } = require('mongodb');
const { getCollection } = require('../utils/db.util');
class CaseSubmission {
/**
* Create a new case submission
*/
static async create(data) {
const collection = await getCollection('case_submissions');
const submission = {
submitter: {
name: data.submitter.name,
email: data.submitter.email,
organization: data.submitter.organization,
public: data.submitter.public !== undefined ? data.submitter.public : false
},
case_study: {
title: data.case_study.title,
description: data.case_study.description,
failure_mode: data.case_study.failure_mode,
tractatus_applicability: data.case_study.tractatus_applicability,
evidence: data.case_study.evidence || [],
attachments: data.case_study.attachments || []
},
ai_review: {
relevance_score: data.ai_review?.relevance_score, // 0-1
completeness_score: data.ai_review?.completeness_score, // 0-1
recommended_category: data.ai_review?.recommended_category,
suggested_improvements: data.ai_review?.suggested_improvements || [],
claude_analysis: data.ai_review?.claude_analysis
},
moderation: {
status: data.moderation?.status || 'pending', // pending/approved/rejected/needs_info
reviewer: data.moderation?.reviewer,
review_notes: data.moderation?.review_notes,
reviewed_at: data.moderation?.reviewed_at
},
published_case_id: data.published_case_id,
submitted_at: new Date()
};
const result = await collection.insertOne(submission);
return { ...submission, _id: result.insertedId };
}
/**
* Find submission by ID
*/
static async findById(id) {
const collection = await getCollection('case_submissions');
return await collection.findOne({ _id: new ObjectId(id) });
}
/**
* Find by moderation status
*/
static async findByStatus(status, options = {}) {
const collection = await getCollection('case_submissions');
const { limit = 20, skip = 0 } = options;
return await collection
.find({ 'moderation.status': status })
.sort({ submitted_at: -1 })
.skip(skip)
.limit(limit)
.toArray();
}
/**
* Find high-relevance submissions pending review
*/
static async findHighRelevance(options = {}) {
const collection = await getCollection('case_submissions');
const { limit = 10 } = options;
return await collection
.find({
'moderation.status': 'pending',
'ai_review.relevance_score': { $gte: 0.7 }
})
.sort({ 'ai_review.relevance_score': -1 })
.limit(limit)
.toArray();
}
/**
* Update submission
*/
static async update(id, updates) {
const collection = await getCollection('case_submissions');
const result = await collection.updateOne(
{ _id: new ObjectId(id) },
{ $set: updates }
);
return result.modifiedCount > 0;
}
/**
* Approve submission
*/
static async approve(id, reviewerId, notes = '') {
const collection = await getCollection('case_submissions');
const result = await collection.updateOne(
{ _id: new ObjectId(id) },
{
$set: {
'moderation.status': 'approved',
'moderation.reviewer': reviewerId,
'moderation.review_notes': notes,
'moderation.reviewed_at': new Date()
}
}
);
return result.modifiedCount > 0;
}
/**
* Reject submission
*/
static async reject(id, reviewerId, reason) {
const collection = await getCollection('case_submissions');
const result = await collection.updateOne(
{ _id: new ObjectId(id) },
{
$set: {
'moderation.status': 'rejected',
'moderation.reviewer': reviewerId,
'moderation.review_notes': reason,
'moderation.reviewed_at': new Date()
}
}
);
return result.modifiedCount > 0;
}
/**
* Request more information
*/
static async requestInfo(id, reviewerId, requestedInfo) {
const collection = await getCollection('case_submissions');
const result = await collection.updateOne(
{ _id: new ObjectId(id) },
{
$set: {
'moderation.status': 'needs_info',
'moderation.reviewer': reviewerId,
'moderation.review_notes': requestedInfo,
'moderation.reviewed_at': new Date()
}
}
);
return result.modifiedCount > 0;
}
/**
* Link to published case study
*/
static async linkPublished(id, publishedCaseId) {
const collection = await getCollection('case_submissions');
const result = await collection.updateOne(
{ _id: new ObjectId(id) },
{
$set: {
published_case_id: new ObjectId(publishedCaseId),
'moderation.status': 'approved'
}
}
);
return result.modifiedCount > 0;
}
/**
* Count by status
*/
static async countByStatus(status) {
const collection = await getCollection('case_submissions');
return await collection.countDocuments({ 'moderation.status': status });
}
/**
* Delete submission
*/
static async delete(id) {
const collection = await getCollection('case_submissions');
const result = await collection.deleteOne({ _id: new ObjectId(id) });
return result.deletedCount > 0;
}
}
module.exports = CaseSubmission;

View file

@ -0,0 +1,143 @@
/**
* Document Model
* Technical papers, framework documentation, specifications
*/
const { ObjectId } = require('mongodb');
const { getCollection } = require('../utils/db.util');
class Document {
/**
* Create a new document
*/
static async create(data) {
const collection = await getCollection('documents');
const document = {
title: data.title,
slug: data.slug,
quadrant: data.quadrant, // STR/OPS/TAC/SYS/STO
persistence: data.persistence, // HIGH/MEDIUM/LOW/VARIABLE
content_html: data.content_html,
content_markdown: data.content_markdown,
toc: data.toc || [],
metadata: {
author: data.metadata?.author || 'John Stroh',
date_created: new Date(),
date_updated: new Date(),
version: data.metadata?.version || '1.0',
document_code: data.metadata?.document_code,
related_documents: data.metadata?.related_documents || [],
tags: data.metadata?.tags || []
},
translations: data.translations || {},
search_index: data.search_index || '',
download_formats: data.download_formats || {}
};
const result = await collection.insertOne(document);
return { ...document, _id: result.insertedId };
}
/**
* Find document by ID
*/
static async findById(id) {
const collection = await getCollection('documents');
return await collection.findOne({ _id: new ObjectId(id) });
}
/**
* Find document by slug
*/
static async findBySlug(slug) {
const collection = await getCollection('documents');
return await collection.findOne({ slug });
}
/**
* Find documents by quadrant
*/
static async findByQuadrant(quadrant, options = {}) {
const collection = await getCollection('documents');
const { limit = 50, skip = 0, sort = { 'metadata.date_created': -1 } } = options;
return await collection
.find({ quadrant })
.sort(sort)
.skip(skip)
.limit(limit)
.toArray();
}
/**
* Search documents
*/
static async search(query, options = {}) {
const collection = await getCollection('documents');
const { limit = 20, skip = 0 } = options;
return await collection
.find(
{ $text: { $search: query } },
{ score: { $meta: 'textScore' } }
)
.sort({ score: { $meta: 'textScore' } })
.skip(skip)
.limit(limit)
.toArray();
}
/**
* Update document
*/
static async update(id, updates) {
const collection = await getCollection('documents');
const result = await collection.updateOne(
{ _id: new ObjectId(id) },
{
$set: {
...updates,
'metadata.date_updated': new Date()
}
}
);
return result.modifiedCount > 0;
}
/**
* Delete document
*/
static async delete(id) {
const collection = await getCollection('documents');
const result = await collection.deleteOne({ _id: new ObjectId(id) });
return result.deletedCount > 0;
}
/**
* List all documents
*/
static async list(options = {}) {
const collection = await getCollection('documents');
const { limit = 50, skip = 0, sort = { 'metadata.date_created': -1 } } = options;
return await collection
.find({})
.sort(sort)
.skip(skip)
.limit(limit)
.toArray();
}
/**
* Count documents
*/
static async count(filter = {}) {
const collection = await getCollection('documents');
return await collection.countDocuments(filter);
}
}
module.exports = Document;

View file

@ -0,0 +1,163 @@
/**
* MediaInquiry Model
* Press/media inquiries with AI triage
*/
const { ObjectId } = require('mongodb');
const { getCollection } = require('../utils/db.util');
class MediaInquiry {
/**
* Create a new media inquiry
*/
static async create(data) {
const collection = await getCollection('media_inquiries');
const inquiry = {
contact: {
name: data.contact.name,
email: data.contact.email,
outlet: data.contact.outlet,
phone: data.contact.phone
},
inquiry: {
subject: data.inquiry.subject,
message: data.inquiry.message,
deadline: data.inquiry.deadline ? new Date(data.inquiry.deadline) : null,
topic_areas: data.inquiry.topic_areas || []
},
ai_triage: {
urgency: data.ai_triage?.urgency, // high/medium/low
topic_sensitivity: data.ai_triage?.topic_sensitivity,
suggested_response_time: data.ai_triage?.suggested_response_time,
involves_values: data.ai_triage?.involves_values || false,
claude_summary: data.ai_triage?.claude_summary,
suggested_talking_points: data.ai_triage?.suggested_talking_points || []
},
status: data.status || 'new', // new/triaged/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('media_inquiries');
return await collection.findOne({ _id: new ObjectId(id) });
}
/**
* Find inquiries by status
*/
static async findByStatus(status, options = {}) {
const collection = await getCollection('media_inquiries');
const { limit = 20, skip = 0 } = options;
return await collection
.find({ status })
.sort({ created_at: -1 })
.skip(skip)
.limit(limit)
.toArray();
}
/**
* Find high urgency inquiries
*/
static async findUrgent(options = {}) {
const collection = await getCollection('media_inquiries');
const { limit = 10 } = options;
return await collection
.find({
'ai_triage.urgency': 'high',
status: { $in: ['new', 'triaged'] }
})
.sort({ created_at: -1 })
.limit(limit)
.toArray();
}
/**
* Update inquiry
*/
static async update(id, updates) {
const collection = await getCollection('media_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('media_inquiries');
const result = await collection.updateOne(
{ _id: new ObjectId(id) },
{
$set: {
assigned_to: new ObjectId(userId),
status: 'triaged'
}
}
);
return result.modifiedCount > 0;
}
/**
* Mark as responded
*/
static async respond(id, responseData) {
const collection = await getCollection('media_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('media_inquiries');
return await collection.countDocuments({ status });
}
/**
* Delete inquiry
*/
static async delete(id) {
const collection = await getCollection('media_inquiries');
const result = await collection.deleteOne({ _id: new ObjectId(id) });
return result.deletedCount > 0;
}
}
module.exports = MediaInquiry;

View file

@ -0,0 +1,210 @@
/**
* ModerationQueue Model
* Human oversight queue for AI actions
*/
const { ObjectId } = require('mongodb');
const { getCollection } = require('../utils/db.util');
class ModerationQueue {
/**
* Add item to moderation queue
*/
static async create(data) {
const collection = await getCollection('moderation_queue');
const item = {
item_type: data.item_type, // blog_post/media_inquiry/case_study/resource
item_id: new ObjectId(data.item_id),
quadrant: data.quadrant, // STR/OPS/TAC/SYS/STO
ai_action: {
type: data.ai_action.type, // suggestion/triage/analysis
confidence: data.ai_action.confidence, // 0-1
reasoning: data.ai_action.reasoning,
claude_version: data.ai_action.claude_version || 'claude-sonnet-4-5'
},
human_required_reason: data.human_required_reason,
priority: data.priority || 'medium', // high/medium/low
assigned_to: data.assigned_to,
status: 'pending', // pending/reviewed/approved/rejected
created_at: new Date(),
reviewed_at: null,
review_decision: {
action: null, // approve/reject/modify/escalate
notes: null,
reviewer: null
}
};
const result = await collection.insertOne(item);
return { ...item, _id: result.insertedId };
}
/**
* Find item by ID
*/
static async findById(id) {
const collection = await getCollection('moderation_queue');
return await collection.findOne({ _id: new ObjectId(id) });
}
/**
* Find pending items
*/
static async findPending(options = {}) {
const collection = await getCollection('moderation_queue');
const { limit = 20, skip = 0, priority } = options;
const filter = { status: 'pending' };
if (priority) filter.priority = priority;
return await collection
.find(filter)
.sort({
priority: -1, // high first
created_at: 1 // oldest first
})
.skip(skip)
.limit(limit)
.toArray();
}
/**
* Find by item type
*/
static async findByType(itemType, options = {}) {
const collection = await getCollection('moderation_queue');
const { limit = 20, skip = 0 } = options;
return await collection
.find({ item_type: itemType, status: 'pending' })
.sort({ created_at: -1 })
.skip(skip)
.limit(limit)
.toArray();
}
/**
* Find by quadrant
*/
static async findByQuadrant(quadrant, options = {}) {
const collection = await getCollection('moderation_queue');
const { limit = 20, skip = 0 } = options;
return await collection
.find({ quadrant, status: 'pending' })
.sort({ created_at: -1 })
.skip(skip)
.limit(limit)
.toArray();
}
/**
* Review item (approve/reject/modify/escalate)
*/
static async review(id, decision) {
const collection = await getCollection('moderation_queue');
const result = await collection.updateOne(
{ _id: new ObjectId(id) },
{
$set: {
status: 'reviewed',
reviewed_at: new Date(),
'review_decision.action': decision.action,
'review_decision.notes': decision.notes,
'review_decision.reviewer': decision.reviewer
}
}
);
return result.modifiedCount > 0;
}
/**
* Approve item
*/
static async approve(id, reviewerId, notes = '') {
return await this.review(id, {
action: 'approve',
notes,
reviewer: reviewerId
});
}
/**
* Reject item
*/
static async reject(id, reviewerId, notes) {
return await this.review(id, {
action: 'reject',
notes,
reviewer: reviewerId
});
}
/**
* Escalate to strategic review
*/
static async escalate(id, reviewerId, reason) {
const collection = await getCollection('moderation_queue');
const result = await collection.updateOne(
{ _id: new ObjectId(id) },
{
$set: {
quadrant: 'STRATEGIC',
priority: 'high',
human_required_reason: `ESCALATED: ${reason}`,
'review_decision.action': 'escalate',
'review_decision.notes': reason,
'review_decision.reviewer': reviewerId
}
}
);
return result.modifiedCount > 0;
}
/**
* Count pending items
*/
static async countPending(filter = {}) {
const collection = await getCollection('moderation_queue');
return await collection.countDocuments({
...filter,
status: 'pending'
});
}
/**
* Get stats by quadrant
*/
static async getStatsByQuadrant() {
const collection = await getCollection('moderation_queue');
return await collection.aggregate([
{ $match: { status: 'pending' } },
{
$group: {
_id: '$quadrant',
count: { $sum: 1 },
high_priority: {
$sum: { $cond: [{ $eq: ['$priority', 'high'] }, 1, 0] }
}
}
}
]).toArray();
}
/**
* Delete item
*/
static async delete(id) {
const collection = await getCollection('moderation_queue');
const result = await collection.deleteOne({ _id: new ObjectId(id) });
return result.deletedCount > 0;
}
}
module.exports = ModerationQueue;

View file

@ -0,0 +1,221 @@
/**
* Resource Model
* Curated directory of aligned resources
*/
const { ObjectId } = require('mongodb');
const { getCollection } = require('../utils/db.util');
class Resource {
/**
* Create a new resource
*/
static async create(data) {
const collection = await getCollection('resources');
const resource = {
url: data.url,
title: data.title,
description: data.description,
category: data.category, // framework/tool/research/organization/educational
subcategory: data.subcategory,
alignment_score: data.alignment_score, // 0-1 alignment with Tractatus values
ai_analysis: {
summary: data.ai_analysis?.summary,
relevance: data.ai_analysis?.relevance,
quality_indicators: data.ai_analysis?.quality_indicators || [],
concerns: data.ai_analysis?.concerns || [],
claude_reasoning: data.ai_analysis?.claude_reasoning
},
status: data.status || 'pending', // pending/approved/rejected
reviewed_by: data.reviewed_by,
reviewed_at: data.reviewed_at,
tags: data.tags || [],
featured: data.featured || false,
added_at: new Date(),
last_checked: new Date()
};
const result = await collection.insertOne(resource);
return { ...resource, _id: result.insertedId };
}
/**
* Find resource by ID
*/
static async findById(id) {
const collection = await getCollection('resources');
return await collection.findOne({ _id: new ObjectId(id) });
}
/**
* Find resource by URL
*/
static async findByUrl(url) {
const collection = await getCollection('resources');
return await collection.findOne({ url });
}
/**
* Find approved resources
*/
static async findApproved(options = {}) {
const collection = await getCollection('resources');
const { limit = 50, skip = 0, category } = options;
const filter = { status: 'approved' };
if (category) filter.category = category;
return await collection
.find(filter)
.sort({ alignment_score: -1, added_at: -1 })
.skip(skip)
.limit(limit)
.toArray();
}
/**
* Find featured resources
*/
static async findFeatured(options = {}) {
const collection = await getCollection('resources');
const { limit = 10 } = options;
return await collection
.find({ status: 'approved', featured: true })
.sort({ alignment_score: -1 })
.limit(limit)
.toArray();
}
/**
* Find by category
*/
static async findByCategory(category, options = {}) {
const collection = await getCollection('resources');
const { limit = 30, skip = 0 } = options;
return await collection
.find({ category, status: 'approved' })
.sort({ alignment_score: -1 })
.skip(skip)
.limit(limit)
.toArray();
}
/**
* Find high-alignment pending resources
*/
static async findHighAlignment(options = {}) {
const collection = await getCollection('resources');
const { limit = 10 } = options;
return await collection
.find({
status: 'pending',
alignment_score: { $gte: 0.8 }
})
.sort({ alignment_score: -1 })
.limit(limit)
.toArray();
}
/**
* Update resource
*/
static async update(id, updates) {
const collection = await getCollection('resources');
const result = await collection.updateOne(
{ _id: new ObjectId(id) },
{ $set: updates }
);
return result.modifiedCount > 0;
}
/**
* Approve resource
*/
static async approve(id, reviewerId) {
const collection = await getCollection('resources');
const result = await collection.updateOne(
{ _id: new ObjectId(id) },
{
$set: {
status: 'approved',
reviewed_by: reviewerId,
reviewed_at: new Date()
}
}
);
return result.modifiedCount > 0;
}
/**
* Reject resource
*/
static async reject(id, reviewerId) {
const collection = await getCollection('resources');
const result = await collection.updateOne(
{ _id: new ObjectId(id) },
{
$set: {
status: 'rejected',
reviewed_by: reviewerId,
reviewed_at: new Date()
}
}
);
return result.modifiedCount > 0;
}
/**
* Mark as featured
*/
static async setFeatured(id, featured = true) {
const collection = await getCollection('resources');
const result = await collection.updateOne(
{ _id: new ObjectId(id) },
{ $set: { featured } }
);
return result.modifiedCount > 0;
}
/**
* Update last checked timestamp
*/
static async updateLastChecked(id) {
const collection = await getCollection('resources');
await collection.updateOne(
{ _id: new ObjectId(id) },
{ $set: { last_checked: new Date() } }
);
}
/**
* Count by status
*/
static async countByStatus(status) {
const collection = await getCollection('resources');
return await collection.countDocuments({ status });
}
/**
* Delete resource
*/
static async delete(id) {
const collection = await getCollection('resources');
const result = await collection.deleteOne({ _id: new ObjectId(id) });
return result.deletedCount > 0;
}
}
module.exports = Resource;

177
src/models/User.model.js Normal file
View file

@ -0,0 +1,177 @@
/**
* User Model
* Admin user accounts
*/
const { ObjectId } = require('mongodb');
const bcrypt = require('bcrypt');
const { getCollection } = require('../utils/db.util');
class User {
/**
* Create a new user
*/
static async create(data) {
const collection = await getCollection('users');
// Hash password
const hashedPassword = await bcrypt.hash(data.password, 10);
const user = {
email: data.email,
password: hashedPassword,
name: data.name,
role: data.role || 'admin', // admin/moderator/viewer
created_at: new Date(),
last_login: null,
active: data.active !== undefined ? data.active : true
};
const result = await collection.insertOne(user);
// Return user without password
const { password, ...userWithoutPassword } = { ...user, _id: result.insertedId };
return userWithoutPassword;
}
/**
* Find user by ID
*/
static async findById(id) {
const collection = await getCollection('users');
const user = await collection.findOne({ _id: new ObjectId(id) });
if (user) {
const { password, ...userWithoutPassword } = user;
return userWithoutPassword;
}
return null;
}
/**
* Find user by email
*/
static async findByEmail(email) {
const collection = await getCollection('users');
return await collection.findOne({ email: email.toLowerCase() });
}
/**
* Authenticate user
*/
static async authenticate(email, password) {
const user = await this.findByEmail(email);
if (!user || !user.active) {
return null;
}
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return null;
}
// Update last login
await this.updateLastLogin(user._id);
// Return user without password
const { password: _, ...userWithoutPassword } = user;
return userWithoutPassword;
}
/**
* Update last login timestamp
*/
static async updateLastLogin(id) {
const collection = await getCollection('users');
await collection.updateOne(
{ _id: new ObjectId(id) },
{ $set: { last_login: new Date() } }
);
}
/**
* Update user
*/
static async update(id, updates) {
const collection = await getCollection('users');
// Remove password from updates (use changePassword for that)
const { password, ...safeUpdates } = updates;
const result = await collection.updateOne(
{ _id: new ObjectId(id) },
{ $set: safeUpdates }
);
return result.modifiedCount > 0;
}
/**
* Change password
*/
static async changePassword(id, newPassword) {
const collection = await getCollection('users');
const hashedPassword = await bcrypt.hash(newPassword, 10);
const result = await collection.updateOne(
{ _id: new ObjectId(id) },
{ $set: { password: hashedPassword } }
);
return result.modifiedCount > 0;
}
/**
* Deactivate user
*/
static async deactivate(id) {
const collection = await getCollection('users');
const result = await collection.updateOne(
{ _id: new ObjectId(id) },
{ $set: { active: false } }
);
return result.modifiedCount > 0;
}
/**
* List all users
*/
static async list(options = {}) {
const collection = await getCollection('users');
const { limit = 50, skip = 0 } = options;
const users = await collection
.find({}, { projection: { password: 0 } })
.sort({ created_at: -1 })
.skip(skip)
.limit(limit)
.toArray();
return users;
}
/**
* Count users
*/
static async count(filter = {}) {
const collection = await getCollection('users');
return await collection.countDocuments(filter);
}
/**
* Delete user
*/
static async delete(id) {
const collection = await getCollection('users');
const result = await collection.deleteOne({ _id: new ObjectId(id) });
return result.deletedCount > 0;
}
}
module.exports = User;

22
src/models/index.js Normal file
View file

@ -0,0 +1,22 @@
/**
* Models Index
* Export all models
*/
const Document = require('./Document.model');
const BlogPost = require('./BlogPost.model');
const MediaInquiry = require('./MediaInquiry.model');
const CaseSubmission = require('./CaseSubmission.model');
const Resource = require('./Resource.model');
const ModerationQueue = require('./ModerationQueue.model');
const User = require('./User.model');
module.exports = {
Document,
BlogPost,
MediaInquiry,
CaseSubmission,
Resource,
ModerationQueue,
User
};