File size: 2,688 Bytes
6e13570
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
const mongoose = require("mongoose");
const bcrypt = require("bcryptjs");

const enrolledCourseSchema = new mongoose.Schema(
  {
    course: { type: mongoose.Schema.Types.ObjectId, ref: "Course", required: true },
    order: { type: mongoose.Schema.Types.ObjectId, ref: "Order", required: true },
    accessGrantedAt: { type: Date, default: Date.now },
  },
  { _id: false }
);

const userSchema = new mongoose.Schema(
  {
    name: { type: String, required: true, trim: true, maxlength: 120 },
    email: {
      type: String,
      required: true,
      unique: true,
      lowercase: true,
      trim: true,
      maxlength: 254,
      index: true,
    },
    passwordHash: { type: String, required: true, select: false },
    role: { type: String, enum: ["user", "admin"], default: "user" },

    // Access-control / session-security bookkeeping
    tokenVersion: { type: Number, default: 0 }, // bump to invalidate all issued JWTs
    activeRefreshTokens: {
      // store only the jti + expiry, never the raw token, so a leaked DB
      // dump can't be replayed as a session
      type: [
        {
          jti: String,
          expiresAt: Date,
          userAgent: String,
          ip: String,
        },
      ],
      default: [],
      select: false,
    },

    // Brute-force protection
    failedLoginAttempts: { type: Number, default: 0, select: false },
    lockUntil: { type: Date, default: null, select: false },

    enrolledCourses: { type: [enrolledCourseSchema], default: [] },

    isActive: { type: Boolean, default: true },
  },
  { timestamps: true }
);

userSchema.methods.setPassword = async function setPassword(plainPassword) {
  const salt = await bcrypt.genSalt(12);
  this.passwordHash = await bcrypt.hash(plainPassword, salt);
};

userSchema.methods.comparePassword = function comparePassword(plainPassword) {
  return bcrypt.compare(plainPassword, this.passwordHash);
};

userSchema.methods.isLocked = function isLocked() {
  return !!(this.lockUntil && this.lockUntil > Date.now());
};

userSchema.methods.hasAccessToCourse = function hasAccessToCourse(courseId) {
  return this.enrolledCourses.some((ec) => ec.course.toString() === courseId.toString());
};

userSchema.methods.toSafeJSON = function toSafeJSON() {
  return {
    id: this._id,
    name: this.name,
    email: this.email,
    role: this.role,
    enrolledCourses: this.enrolledCourses.map((ec) => ({
      course: ec.course,
      order: ec.order,
      accessGrantedAt: ec.accessGrantedAt,
    })),
    createdAt: this.createdAt,
  };
};

module.exports = mongoose.model("User", userSchema);