Untitled

mail@pastecode.io avatar
unknown
typescript
4 months ago
4.2 kB
2
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;
Leave a Comment