tractatus/src/models/User.model.js
TheFlow 2298d36bed fix(submissions): restructure Economist package and fix article display
- Create Economist SubmissionTracking package correctly:
  * mainArticle = full blog post content
  * coverLetter = 216-word SIR— letter
  * Links to blog post via blogPostId
- Archive 'Letter to The Economist' from blog posts (it's the cover letter)
- Fix date display on article cards (use published_at)
- Target publication already displaying via blue badge

Database changes:
- Make blogPostId optional in SubmissionTracking model
- Economist package ID: 68fa85ae49d4900e7f2ecd83
- Le Monde package ID: 68fa2abd2e6acd5691932150

Next: Enhanced modal with tabs, validation, export

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 08:47:42 +13:00

177 lines
3.9 KiB
JavaScript

/**
* 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;