Untitled
unknown
typescript
a year ago
4.2 kB
10
Indexable
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;Editor is loading...
Leave a Comment