import mongoose, { Document } from "mongoose"; import validator from "validator"; import crypto from "crypto"; import jwt from "jsonwebtoken"; import env from "../enviroment/env"; import NotAuthorizedError from "../utils/NotAuthorizedError"; import { encryptToken as encryptTokenUtil, decryptToken as decryptTokenUtil, getEncryptionKey as getEncryptionKeyUtil, generateEncryptionKeyMaterial, hashPassword, comparePassword, signAuthTokens, signStreamVideoToken, signTempAuthToken, signEmailVerifyToken, signPasswordResetToken, } from "../utils/userCrypto"; const userSchema = new mongoose.Schema( { email: { type: String, required: true, trim: true, unique: true, lowercase: true, validate(value: any): any { if (!validator.isEmail(value)) { throw new Error("Email is invalid"); } else if (value.length > 320) { throw new Error("Email length must be less than 320 characters"); } else if (value.length < 3) { throw new Error("Email length must be at least 3 characters"); } }, }, password: { type: String, trim: true, required: true, validate(value: any): any { if (value.length < 6) { throw new Error("Password length must be at least 6 characters"); } else if (value.length > 256) { throw new Error("Password length must be less than 256 characters"); } }, }, tokens: [ { token: { type: String, required: true, }, uuid: { type: String, required: true, }, time: { type: Number, required: true, }, }, ], tempTokens: [ { token: { type: String, required: true, }, uuid: { type: String, required: true, }, time: { type: Number, required: true, }, }, ], privateKey: { type: String, }, publicKey: { type: String, }, emailVerified: { type: Boolean, }, emailToken: { type: String, }, passwordResetToken: { type: String, }, passwordLastModified: Number, }, { timestamps: true, } ); export interface UserInterface extends Document { _id: mongoose.Types.ObjectId; email: string; password: string; tokens: any[]; tempTokens: any[]; privateKey?: string; publicKey?: string; token?: string; emailVerified?: boolean; emailToken?: string; passwordResetToken?: string; passwordLastModified?: number; getEncryptionKey: () => Buffer | undefined; generateTempAuthToken: () => Promise; encryptToken: (tempToken: any, key: any, publicKey: any) => any; decryptToken: (encryptedToken: any, key: any, publicKey: any) => any; findByCreds: (email: string, password: string) => Promise; generateAuthToken: ( uuid: string | undefined ) => Promise<{ accessToken: string; refreshToken: string }>; generateAuthTokenStreamVideo: (uuid: string | undefined) => Promise; generateEncryptionKeys: () => Promise; changeEncryptionKey: (randomKey: Buffer) => Promise; generateEmailVerifyToken: () => Promise; generatePasswordResetToken: () => Promise; } userSchema.pre("save", async function (this: any, next: any) { const user = this; if (user.isModified("password")) { user.password = await hashPassword(user.password); } next(); }); userSchema.statics.findByCreds = async (email: string, password: string) => { const user = await User.findOne({ email }); if (!user) { throw new NotAuthorizedError("User not found"); } const isMatch = await comparePassword(password, user.password); if (!isMatch) { throw new NotAuthorizedError("Incorrect password"); } return user; }; userSchema.methods.toJSON = function () { const user = this; const userObject = user.toObject(); delete userObject.password; delete userObject.tokens; delete userObject.tempTokens; delete userObject.privateKey; delete userObject.publicKey; if (env.emailVerification !== "true") { delete userObject.emailVerified; } else { userObject.emailVerified = user.emailVerified || false; } return userObject; }; userSchema.methods.generateAuthTokenStreamVideo = async function ( uuid: string | undefined ) { const user = this; const { token, encryptedToken, time } = signStreamVideoToken(user); uuid = uuid ? uuid : "unknown"; await User.updateOne( { _id: user._id }, { $push: { tempTokens: { token: encryptedToken, uuid, time } } } ); return token; }; userSchema.methods.generateAuthToken = async function ( uuid: string | undefined ) { const user = this; const { accessToken, refreshToken, encryptedRefreshToken, time } = signAuthTokens(user); uuid = uuid ? uuid : "unknown"; await User.updateOne( { _id: user._id }, { $push: { tokens: { token: encryptedRefreshToken, uuid, time }, }, } ); return { accessToken, refreshToken }; }; userSchema.methods.encryptToken = function ( token: string, key: string, iv: any ) { return encryptTokenUtil(token, key, iv); }; userSchema.methods.decryptToken = function ( encryptedToken: any, key: string, iv: any ) { return decryptTokenUtil(encryptedToken, key, iv); }; userSchema.methods.generateEncryptionKeys = async function () { const user = this; const randomKey = crypto.randomBytes(32); const { privateKey, publicKey } = generateEncryptionKeyMaterial( user.password, randomKey ); user.privateKey = privateKey; user.publicKey = publicKey; await user.save(); }; userSchema.methods.getEncryptionKey = function () { return getEncryptionKeyUtil(this); }; userSchema.methods.changeEncryptionKey = async function (randomKey: Buffer) { const user = this; const { privateKey, publicKey } = generateEncryptionKeyMaterial( user.password, randomKey ); user.privateKey = privateKey; user.publicKey = publicKey; await user.save(); }; userSchema.methods.generateTempAuthToken = async function () { const user = this as UserInterface; const { token, encryptedToken } = signTempAuthToken(user); user.tempTokens = user.tempTokens.concat({ token: encryptedToken }); await user.save(); return token; }; userSchema.methods.generateEmailVerifyToken = async function () { const user = this as UserInterface; const { token, encryptedToken } = signEmailVerifyToken(user); user.emailToken = encryptedToken; await user.save(); return token; }; userSchema.methods.generatePasswordResetToken = async function () { const user = this as UserInterface; const { token, encryptedToken } = signPasswordResetToken(user); user.passwordResetToken = encryptedToken; await user.save(); return token; }; userSchema.methods.generateTempAuthTokenVideo = async function ( cookie: string ) { const iv = crypto.randomBytes(16); const user = this; const token = jwt.sign( { _id: user._id.toString(), cookie, iv }, env.passwordAccess!, { expiresIn: "5h" } ); const encryptionKey = user.getEncryptionKey(); const encryptedToken = user.encryptToken(token, encryptionKey, iv); user.tempTokens = user.tempTokens.concat({ token: encryptedToken }); await user.save(); return token; }; const User = mongoose.model("User", userSchema); export default User; module.exports = User;