const bcrypt = require("bcryptjs"); const couchdbService = require("../services/couchdbService"); const { ValidationError, withErrorHandling, createErrorContext, } = require("../utils/modelErrors"); const URL_REGEX = /^(https?|ftp):\/\/[^\s\/$.?#].[^\s]*$/i; class User { constructor(data) { // Validate required fields for new user creation if (!data._id) { // Only for new users if (!data.name) { throw new ValidationError("Name is required", "name", data.name); } if (!data.email) { throw new ValidationError("Email is required", "email", data.email); } if (!data.password) { throw new ValidationError("Password is required", "password", data.password); } } this._id = data._id || null; this._rev = data._rev || null; this.type = "user"; this.name = data.name; this.email = data.email; this.password = data.password; // --- Profile Information --- this.avatar = data.avatar || null; this.profilePicture = data.profilePicture || data.avatar || null; this.cloudinaryPublicId = data.cloudinaryPublicId || null; this.bio = data.bio || ""; if (this.bio.length > 500) { throw new ValidationError("Bio cannot exceed 500 characters.", "bio", this.bio); } this.location = data.location || ""; this.website = data.website || ""; if (this.website && !URL_REGEX.test(this.website)) { throw new ValidationError("Invalid website URL.", "website", this.website); } // --- Social Links --- this.social = data.social || { twitter: "", github: "", linkedin: "" }; if (this.social.twitter && !URL_REGEX.test(this.social.twitter)) { throw new ValidationError("Invalid Twitter URL.", "social.twitter", this.social.twitter); } if (this.social.github && !URL_REGEX.test(this.social.github)) { throw new ValidationError("Invalid Github URL.", "social.github", this.social.github); } if (this.social.linkedin && !URL_REGEX.test(this.social.linkedin)) { throw new ValidationError("Invalid LinkedIn URL.", "social.linkedin", this.social.linkedin); } // --- Settings & Preferences --- this.privacySettings = data.privacySettings || { profileVisibility: "public" }; if (!["public", "private"].includes(this.privacySettings.profileVisibility)) { throw new ValidationError("Profile visibility must be 'public' or 'private'.", "privacySettings.profileVisibility", this.privacySettings.profileVisibility); } this.preferences = data.preferences || { emailNotifications: true, pushNotifications: true, theme: "light" }; if (!["light", "dark"].includes(this.preferences.theme)) { throw new ValidationError("Theme must be 'light' or 'dark'.", "preferences.theme", this.preferences.theme); } // --- Gamification & App Data --- this.isPremium = data.isPremium || false; this.points = Math.max(0, data.points || 0); this.adoptedStreets = data.adoptedStreets || []; this.completedTasks = data.completedTasks || []; this.posts = data.posts || []; this.events = data.events || []; this.earnedBadges = data.earnedBadges || []; this.stats = data.stats || { streetsAdopted: 0, tasksCompleted: 0, postsCreated: 0, eventsParticipated: 0, badgesEarned: 0, }; this.createdAt = data.createdAt || new Date().toISOString(); this.updatedAt = data.updatedAt || new Date().toISOString(); } // ... (static methods remain the same) // Static methods for MongoDB compatibility static async findOne(query) { const errorContext = createErrorContext('User', 'findOne', { query }); return await withErrorHandling(async () => { let user; if (query.email) { user = await couchdbService.findUserByEmail(query.email); } else if (query._id) { user = await couchdbService.findUserById(query._id); } else { // Generic query fallback const docs = await couchdbService.find({ selector: { type: "user", ...query }, limit: 1 }); user = docs[0] || null; } return user ? new User(user) : null; }, errorContext); } static async findById(id) { const errorContext = createErrorContext('User', 'findById', { id }); return await withErrorHandling(async () => { const user = await couchdbService.findUserById(id); return user ? new User(user) : null; }, errorContext); } static async findByIdAndUpdate(id, update, options = {}) { const errorContext = createErrorContext('User', 'findByIdAndUpdate', { id, update, options }); return await withErrorHandling(async () => { const user = await couchdbService.findUserById(id); if (!user) return null; const updatedUser = { ...user, ...update, updatedAt: new Date().toISOString() }; const saved = await couchdbService.update(id, updatedUser); if (options.new) { return new User(saved); } return new User(user); }, errorContext); } static async findByIdAndDelete(id) { const errorContext = createErrorContext('User', 'findByIdAndDelete', { id }); return await withErrorHandling(async () => { const user = await couchdbService.findUserById(id); if (!user) return null; await couchdbService.delete(id); return user; }, errorContext); } static async find(query = {}) { const errorContext = createErrorContext('User', 'find', { query }); return await withErrorHandling(async () => { const selector = { type: "user", ...query }; const users = await couchdbService.find({ selector }); return users.map(u => new User(u)); }, errorContext); } static async create(userData) { const errorContext = createErrorContext('User', 'create', { userData: { ...userData, password: '[REDACTED]' } }); return await withErrorHandling(async () => { const user = new User(userData); if (user.password) { const salt = await bcrypt.genSalt(10); user.password = await bcrypt.hash(user.password, salt); } if (!user._id) { user._id = `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } const created = await couchdbService.createDocument(user.toJSON()); return new User(created); }, errorContext); } async save() { const errorContext = createErrorContext('User', 'save', { id: this._id, email: this.email, isNew: !this._id }); return await withErrorHandling(async () => { this.updatedAt = new Date().toISOString(); if (!this._id) { this._id = `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; if (this.password && !this.password.startsWith('$2')) { const salt = await bcrypt.genSalt(10); this.password = await bcrypt.hash(this.password, salt); } const created = await couchdbService.createDocument(this.toJSON()); this._rev = created._rev; return this; } else { const updated = await couchdbService.updateDocument(this.toJSON()); this._rev = updated._rev; return this; } }, errorContext); } async comparePassword(candidatePassword) { const errorContext = createErrorContext('User', 'comparePassword', { id: this._id, email: this.email }); return await withErrorHandling(async () => { return await bcrypt.compare(candidatePassword, this.password); }, errorContext); } toSafeObject() { const obj = this.toJSON(); delete obj.password; return obj; } toJSON() { return { _id: this._id, _rev: this._rev, type: this.type, name: this.name, email: this.email, password: this.password, avatar: this.avatar, profilePicture: this.profilePicture, cloudinaryPublicId: this.cloudinaryPublicId, bio: this.bio, location: this.location, website: this.website, social: this.social, privacySettings: this.privacySettings, preferences: this.preferences, isPremium: this.isPremium, points: this.points, adoptedStreets: this.adoptedStreets, completedTasks: this.completedTasks, posts: this.posts, events: this.events, earnedBadges: this.earnedBadges, stats: this.stats, createdAt: this.createdAt, updatedAt: this.updatedAt, }; } } module.exports = User;