import * as mongoose from 'mongoose' ; import * as bcrypt from 'bcrypt' ; import { IUser } from '../../../shared/models/IUser' ; import { IUserSubSafe } from '../../../shared/models/IUserSubSafe' ; import { IUserSubTeacher } from '../../../shared/models/IUserSubTeacher' ; import { IUserSubCourseView } from '../../../shared/models/IUserSubCourseView' ; import { NativeError } from 'mongoose' ; import * as crypto from 'crypto' ; import { isNullOrUndefined } from 'util' ; import { isEmail } from 'validator' ; import { errorCodes } from '../config/errorCodes' ; import { allRoles } from '../config/roles' ; import { extractMongoId } from '../utilities/ExtractMongoId' ; import { ensureMongoToObject } from '../utilities/EnsureMongoToObject' ; import { Course , ICourseModel } from './Course' ; import { NotificationSettings } from './NotificationSettings' ; import { Notification } from './Notification' ; import { WhitelistUser } from './WhitelistUser' ; import { Progress } from './progress/Progress' ; import fs = require ( 'fs' ) ; export interface IUserPrivileges { userIsAdmin : boolean ; userIsTeacher : boolean ; userIsStudent : boolean ; userEditLevel : number ; } export interface IUserEditPrivileges extends IUserPrivileges { currentEditLevel : number ; targetEditLevel : number ; editSelf : boolean ; editLevelHigher : boolean ; editAllowed : boolean ; } interface IUserModel extends IUser , mongoose . Document { exportPersonalData : ( ) => Promise < IUser > ; isValidPassword : ( candidatePassword : string ) => Promise < boolean > ; checkPrivileges : ( ) => IUserPrivileges ; checkEditUser : ( targetUser : IUser ) => IUserEditPrivileges ; checkEditableBy : ( currentUser : IUser ) => IUserEditPrivileges ; forSafe : ( ) => IUserSubSafe ; forTeacher : ( ) => IUserSubTeacher ; forCourseView : ( ) => IUserSubCourseView ; forUser : ( otherUser : IUser ) => IUserSubSafe | IUserSubTeacher | IUser ; authenticationToken : string ; resetPasswordToken : string ; resetPasswordExpires : Date ; isActive : boolean ; updatedAt : Date ; } interface IUserMongoose extends mongoose . Model < IUserModel > { getEditLevel : ( user : IUser ) => number ; getEditLevelUnsafe : ( user : any ) => number | undefined ; checkPrivileges : ( user : IUser ) => IUserPrivileges ; checkEditUser : ( currentUser : IUser , targetUser : IUser ) => IUserEditPrivileges ; forSafe : ( user : IUser | IUserModel ) => IUserSubSafe ; forTeacher : ( user : IUser | IUserModel ) => IUserSubTeacher ; forCourseView : ( user : IUser | IUserModel ) => IUserSubCourseView ; forUser : ( user : IUser | IUserModel , otherUser : IUser ) => IUserSubSafe | IUserSubTeacher | IUser ; } let User ; const userSchema = new mongoose . Schema ( { uid : { type : String , lowercase : true , unique : true , sparse : true , index : true } , email : { type : String , lowercase : true , unique : true , required : true , validate : [ { validator : ( value ) => isEmail ( value ) , msg : 'Invalid email.' } ] , index : true } , password : { type : String , required : true , validate : new RegExp ( errorCodes . password . regex . regex ) } , profile : { firstName : { type : String , index : true , maxlength : 64 } , lastName : { type : String , index : true , maxlength : 64 } , picture : { path : { type : String } , name : { type : String } , alias : { type : String } } , theme : { type : String } } , role : { type : String , 'enum' : allRoles , 'default' : 'student' } , lastVisitedCourses : [ { type : String } ] , authenticationToken : { type : String } , resetPasswordToken : { type : String } , resetPasswordExpires : { type : Date } , isActive : { type : Boolean , 'default' : false } , updatedAt : { type : Date , required : true , default : Date . now } } , { timestamps : true , toObject : { virtuals : true , transform : function ( doc , ret ) { ret . _id = ret . _id . toString ( ) ; delete ret . password ; } } } ) ; userSchema . index ( { uid : 'text' , email : 'text' , 'profile.firstName' : 'text' , 'profile.lastName' : 'text' } , { name : 'user_combined' } ) ; function hashPassword ( next ) { const user = this , SALT_FACTOR = 5 ; if ( ! user . isModified ( 'password' ) ) { return next ( ) ; } bcrypt . hash ( user . password , SALT_FACTOR ) . then ( ( hash ) => { user . password = hash ; } ) . then ( ( ) => next ( ) ) . catch ( next ) ; } function generateActivationToken ( next ) { if ( ! this . isActive && isNullOrUndefined ( this . authenticationToken ) ) { this . authenticationToken = generateSecureToken ( ) ; } next ( ) ; } function generatePasswordResetToken ( next ) { if ( ! isNullOrUndefined ( this . resetPasswordExpires ) && isNullOrUndefined ( this . resetPasswordToken ) ) { this . resetPasswordToken = generateSecureToken ( ) ; } next ( ) ; } function generateSecureToken ( ) { return crypto . randomBytes ( 64 ) . toString ( 'base64' ) ; } function removeEmptyUid ( next ) { if ( this . uid != null && this . uid . length === 0 ) { this . uid = undefined ; } next ( ) ; } userSchema . pre ( 'save' , hashPassword ) ; userSchema . pre ( 'save' , generateActivationToken ) ; userSchema . pre ( 'save' , generatePasswordResetToken ) ; userSchema . pre ( 'save' , removeEmptyUid ) ; userSchema . pre ( 'findOneAndUpdate' , function ( next ) { const SALT_FACTOR = 5 ; const newPassword = this . getUpdate ( ) . password ; if ( typeof newPassword !== 'undefined' ) { bcrypt . hash ( newPassword , SALT_FACTOR ) . then ( ( hash ) => { this . findOneAndUpdate ( { } , { password : hash } ) ; } ) . then ( ( ) => next ( ) ) . catch ( next ) ; } else { next ( ) ; } } ) ; userSchema . pre ( 'remove' , async function ( ) { const localUser = < IUserModel > < any > this ; try { const promises = [ ] ; promises . push ( Notification . deleteMany ( { user : localUser . _id } ) ) ; promises . push ( NotificationSettings . deleteMany ( { user : localUser . _id } ) ) ; promises . push ( WhitelistUser . deleteMany ( { uid : localUser . uid } ) ) ; promises . push ( Course . updateMany ( { $or : [ { students : localUser . _id } , { teachers : localUser . _id } ] } , { $pull : { 'students' : localUser . _id , 'teachers' : localUser . _id } } ) ) ; promises . push ( Progress . deleteMany ( { user : localUser . _id } ) ) ; const path = localUser . profile . picture . path ; if ( path && fs . existsSync ( path ) ) { fs . unlinkSync ( path ) ; } await Promise . all ( promises ) ; } catch ( e ) { throw new Error ( 'Delete Error: ' + e . toString ( ) ) ; } } ) ; userSchema . methods . isValidPassword = function ( candidatePassword ) { if ( typeof candidatePassword === 'undefined' ) { candidatePassword = '' ; } return bcrypt . compare ( candidatePassword , this . password ) ; } ; userSchema . methods . checkPrivileges = function ( ) : IUserPrivileges { return User . checkPrivileges ( this ) ; } ; userSchema . methods . checkEditUser = function ( targetUser ) : IUserEditPrivileges { return User . checkEditUser ( this , targetUser ) ; } ; userSchema . methods . checkEditableBy = function ( currentUser ) : IUserEditPrivileges { return User . checkEditUser ( currentUser , this ) ; } ; userSchema . methods . forSafe = function ( ) : IUserSubSafe { return User . forSafe ( this ) ; } ; userSchema . methods . forTeacher = function ( ) : IUserSubTeacher { return User . forTeacher ( this ) ; } ; userSchema . methods . forCourseView = function ( ) : IUserSubCourseView { return User . forCourseView ( this ) ; } ; userSchema . methods . forUser = function ( otherUser ) : IUserSubSafe | IUserSubTeacher | IUser { return User . forUser ( this , otherUser ) ; } ; userSchema . methods . exportPersonalData = async function ( ) { await this . populate ( { path : 'lastVisitedCourses' , model : Course , select : 'name description -_id teachers' } ) . execPopulate ( ) ; const lastVisitedCourses = await Promise . all ( this . lastVisitedCourses . map ( async ( course ) => { return await course . exportJSON ( true , true ) ; } ) ) ; const obj = this . toObject ( ) ; obj . lastVisitedCourses = lastVisitedCourses ; delete obj . _id ; delete obj . createdAt ; delete obj . __v ; delete obj . updatedAt ; delete obj . id ; return obj ; } ; userSchema . methods . getCourses = async function ( ) { const localUser = < IUserModel > < any > this ; return Course . find ( { courseAdmin : localUser . _id } ) ; } ; const editLevels = { student : 0 , teacher : 0 , admin : 2 , } ; userSchema . statics . getEditLevel = function ( user ) : number { return editLevels [ user . role ] ; } ; userSchema . statics . getEditLevelUnsafe = function ( user ) : number | undefined { return editLevels [ user . role ] ; } ; userSchema . statics . checkPrivileges = function ( user ) : IUserPrivileges { const userIsAdmin = user . role === 'admin' ; const userIsTeacher = user . role === 'teacher' ; const userIsStudent = user . role === 'student' ; const userEditLevel = User . getEditLevel ( user ) ; return { userIsAdmin , userIsTeacher , userIsStudent , userEditLevel } ; } ; userSchema . statics . checkEditUser = function ( currentUser , targetUser ) : IUserEditPrivileges { const { userIsAdmin , ... userIs } = User . checkPrivileges ( currentUser ) ; const currentEditLevel = User . getEditLevel ( currentUser ) ; const targetEditLevel = User . getEditLevel ( targetUser ) ; const editSelf = extractMongoId ( currentUser . _id ) === extractMongoId ( targetUser . _id ) ; const editLevelHigher = currentEditLevel > targetEditLevel ; const editAllowed = userIsAdmin || editSelf || editLevelHigher ; return { userIsAdmin , ... userIs , currentEditLevel , targetEditLevel , editSelf , editLevelHigher , editAllowed } ; } ; userSchema . statics . forSafe = function ( user ) : IUserSubSafe { const { profile : { firstName , lastName } } = user ; const result = { _id : < string > extractMongoId ( user . _id ) , profile : { firstName , lastName } } ; const picture = ensureMongoToObject ( user . profile . picture ) ; if ( Object . keys ( picture ) . length ) { result . profile . picture = picture ; } return result ; } ; userSchema . statics . forTeacher = function ( user ) : IUserSubTeacher { const { uid , email } = user ; return { ... User . forSafe ( user ) , uid , email } ; } ; userSchema . statics . forCourseView = function ( user ) : IUserSubCourseView { const { email } = user ; return { ... User . forSafe ( user ) , email } ; } ; userSchema . statics . forUser = function ( user , otherUser ) : IUserSubSafe | IUserSubTeacher | IUser { const { userIsTeacher , userIsAdmin } = User . checkPrivileges ( otherUser ) ; const isSelf = extractMongoId ( user . _id ) === extractMongoId ( otherUser . _id ) ; if ( isSelf || userIsAdmin ) { return ensureMongoToObject ( user ) ; } else if ( userIsTeacher ) { return User . forTeacher ( user ) ; } else { return User . forSafe ( user ) ; } } ; User = mongoose . model < IUserModel , IUserMongoose > ( 'User' , userSchema ) ; export { User , IUserModel } ;