Untitled
import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; import { model, Schema } from "mongoose"; import paginate from "mongoose-paginate-v2"; import { emailRegex, LOCK_TIME } from "../../constants"; import HttpError from "../../errors/httpError"; import { IUserDocument, IUserModel } from "../../interfaces"; const schema = new Schema<IUserDocument>({ name: { type: String, required: [true, 'User name is required'], trim: true, }, email: { type: String, required: [true, 'User email is required'], unique: true, validate: { validator: function (v: string) { return emailRegex.test(v); }, message: 'Please enter a valid email address' }, }, address: { type: String, trim: true, validate: { validator: function (v: string) { return v.length <= 100; }, message: 'Address should not exceed 100 characters' } }, password: { type: String, required: [true, 'User password is required'], validate: { validator: function (v: string) { return /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%.\/*?&'"{}()\[\]<>~#-+;:=,])[A-Za-z\d@$!%.\/*?&'"{}()\[\]~#-+;:=,<>]{8,}$/.test(v); }, message: 'Password should be at least 8 characters long, contain at least one uppercase letter, one lowercase letter, one number, and one special character' }, }, avatar: { type: String, trim: true, }, ipAddress: { type: String, trim: true, select: false, }, role: { type: String, enum: { values: ['user', 'admin'], message: '{VALUE} is not supported' }, default: 'user', }, phone: { type: String, required: [true, 'User phone number is required'], validate: { validator: function (v: string) { return /^\+?[1-9]?\d{1,14}$/.test(v); }, message: props => `${props.value} is not valid, Please enter a valid phone number` } }, isVerified: { type: Boolean, default: false, }, status: { type: String, enum: { values: ['active', 'inactive'], message: '{VALUE} is not supported' }, default: 'active', }, loginAttempts: { type: Number, required: true, default: 0, }, lockUntill: { type: Number, }, wishList: [ { type: Schema.Types.ObjectId, ref: 'Product', } ], }, { timestamps: true }); schema.plugin(paginate); schema.methods.toJSON = function () { const user = this.toObject(); delete user.password; delete user.ipAddress; delete user.loginAttempts; delete user.lockUntill; delete user.__v; delete user.createdAt; delete user.updatedAt; return JSON.parse(JSON.stringify(user).replace(/_id/g, 'id')); }; schema.pre('save', async function (next) { const user = this; if (this.isModified('password') || this.isModified('email')) { if (this.isModified('password')) { user.password = await bcrypt.hash(user.password, 10); } if (this.isModified('email')) { this.isVerified = false; } next(); } next(); }); schema.static('isEmailAlreadyTaken', async function isEmailAlreadyTaken(email: string) { const user = await this.findOne({ email }); return !!user; }); schema.method('generateToken', function generateToken() { return jwt.sign({ id: this._id, email: this.email }, process.env.SECRET_KEY!); }); schema.method('comparePassword', async function comparePassword(password: string) { if (this.isLocked) { throw new HttpError('Account is temporarily locked due to too many failed login attempts', 429); } const isMatch = await bcrypt.compare(password, this.password); if (isMatch) { this.loginAttempts = 0; this.lockUntill = undefined; await this.save(); return true; } if (this.loginAttempts < 5) { this.loginAttempts += 1; await this.save(); } if (this.loginAttempts >= 5) { this.loginAttempts = 0; this.lockUntill = Date.now() + LOCK_TIME; await this.save(); return false; }; }); schema.virtual('isLocked').get(function () { return !!(this.lockUntill && this.lockUntill > Date.now()); }); const User = model<IUserDocument, IUserModel>('User', schema, 'users'); export default User;
Leave a Comment