- 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>
177 lines
3.9 KiB
JavaScript
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;
|