Files
rentall-app/backend/models/User.js
2025-10-10 22:54:45 -04:00

294 lines
7.2 KiB
JavaScript

const { DataTypes } = require("sequelize");
const sequelize = require("../config/database");
const bcrypt = require("bcryptjs");
const User = sequelize.define(
"User",
{
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
username: {
type: DataTypes.STRING,
unique: true,
allowNull: true,
},
email: {
type: DataTypes.STRING,
unique: true,
allowNull: true,
validate: {
isEmail: true,
},
},
password: {
type: DataTypes.STRING,
allowNull: true,
},
firstName: {
type: DataTypes.STRING,
allowNull: false,
},
lastName: {
type: DataTypes.STRING,
allowNull: false,
},
phone: {
type: DataTypes.STRING,
allowNull: true,
},
authProvider: {
type: DataTypes.ENUM("local", "google"),
defaultValue: "local",
},
providerId: {
type: DataTypes.STRING,
allowNull: true,
},
address1: {
type: DataTypes.STRING,
},
address2: {
type: DataTypes.STRING,
},
city: {
type: DataTypes.STRING,
},
state: {
type: DataTypes.STRING,
},
zipCode: {
type: DataTypes.STRING,
},
country: {
type: DataTypes.STRING,
},
profileImage: {
type: DataTypes.STRING,
},
isVerified: {
type: DataTypes.BOOLEAN,
defaultValue: false,
},
verificationToken: {
type: DataTypes.STRING,
allowNull: true,
},
verificationTokenExpiry: {
type: DataTypes.DATE,
allowNull: true,
},
verifiedAt: {
type: DataTypes.DATE,
allowNull: true,
},
passwordResetToken: {
type: DataTypes.STRING,
allowNull: true,
},
passwordResetTokenExpiry: {
type: DataTypes.DATE,
allowNull: true,
},
defaultAvailableAfter: {
type: DataTypes.STRING,
defaultValue: "09:00",
},
defaultAvailableBefore: {
type: DataTypes.STRING,
defaultValue: "17:00",
},
defaultSpecifyTimesPerDay: {
type: DataTypes.BOOLEAN,
defaultValue: false,
},
defaultWeeklyTimes: {
type: DataTypes.JSONB,
defaultValue: {
sunday: { availableAfter: "09:00", availableBefore: "17:00" },
monday: { availableAfter: "09:00", availableBefore: "17:00" },
tuesday: { availableAfter: "09:00", availableBefore: "17:00" },
wednesday: { availableAfter: "09:00", availableBefore: "17:00" },
thursday: { availableAfter: "09:00", availableBefore: "17:00" },
friday: { availableAfter: "09:00", availableBefore: "17:00" },
saturday: { availableAfter: "09:00", availableBefore: "17:00" },
},
},
stripeConnectedAccountId: {
type: DataTypes.STRING,
allowNull: true,
},
stripeCustomerId: {
type: DataTypes.STRING,
allowNull: true,
},
loginAttempts: {
type: DataTypes.INTEGER,
defaultValue: 0,
},
lockUntil: {
type: DataTypes.DATE,
allowNull: true,
},
jwtVersion: {
type: DataTypes.INTEGER,
defaultValue: 0,
allowNull: false,
},
},
{
hooks: {
beforeCreate: async (user) => {
if (user.password) {
user.password = await bcrypt.hash(user.password, 12);
}
},
beforeUpdate: async (user) => {
if (user.changed("password") && user.password) {
user.password = await bcrypt.hash(user.password, 12);
}
},
},
}
);
User.prototype.comparePassword = async function (password) {
if (!this.password) {
return false;
}
return bcrypt.compare(password, this.password);
};
// Account lockout constants
const MAX_LOGIN_ATTEMPTS = 5;
const LOCK_TIME = 2 * 60 * 60 * 1000; // 2 hours
// Check if account is locked
User.prototype.isLocked = function () {
return !!(this.lockUntil && this.lockUntil > Date.now());
};
// Increment login attempts and lock account if necessary
User.prototype.incLoginAttempts = async function () {
// If we have a previous lock that has expired, restart at 1
if (this.lockUntil && this.lockUntil < Date.now()) {
return this.update({
loginAttempts: 1,
lockUntil: null,
});
}
const updates = { loginAttempts: this.loginAttempts + 1 };
// Lock account after max attempts
if (this.loginAttempts + 1 >= MAX_LOGIN_ATTEMPTS && !this.isLocked()) {
updates.lockUntil = Date.now() + LOCK_TIME;
}
return this.update(updates);
};
// Reset login attempts after successful login
User.prototype.resetLoginAttempts = async function () {
return this.update({
loginAttempts: 0,
lockUntil: null,
});
};
// Email verification methods
User.prototype.generateVerificationToken = async function () {
const crypto = require("crypto");
const token = crypto.randomBytes(32).toString("hex");
const expiry = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours
return this.update({
verificationToken: token,
verificationTokenExpiry: expiry,
});
};
User.prototype.isVerificationTokenValid = function (token) {
if (!this.verificationToken || !this.verificationTokenExpiry) {
return false;
}
if (this.verificationToken !== token) {
return false;
}
if (new Date() > new Date(this.verificationTokenExpiry)) {
return false;
}
return true;
};
User.prototype.verifyEmail = async function () {
return this.update({
isVerified: true,
verifiedAt: new Date(),
verificationToken: null,
verificationTokenExpiry: null,
});
};
// Password reset methods
User.prototype.generatePasswordResetToken = async function () {
const crypto = require("crypto");
// Generate random token for email URL
const token = crypto.randomBytes(32).toString("hex");
// Hash token before storing in database (SHA-256)
const hashedToken = crypto.createHash("sha256").update(token).digest("hex");
const expiry = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
await this.update({
passwordResetToken: hashedToken,
passwordResetTokenExpiry: expiry,
});
// Return plain token for email URL (not stored in DB)
return token;
};
User.prototype.isPasswordResetTokenValid = function (token) {
if (!this.passwordResetToken || !this.passwordResetTokenExpiry) {
return false;
}
// Check if token is expired first
if (new Date() > new Date(this.passwordResetTokenExpiry)) {
return false;
}
const crypto = require("crypto");
// Hash the incoming token to compare with stored hash
const hashedToken = crypto.createHash("sha256").update(token).digest("hex");
// Use timing-safe comparison to prevent timing attacks
const storedTokenBuffer = Buffer.from(this.passwordResetToken, "hex");
const hashedTokenBuffer = Buffer.from(hashedToken, "hex");
// Ensure buffers are same length for timingSafeEqual
if (storedTokenBuffer.length !== hashedTokenBuffer.length) {
return false;
}
return crypto.timingSafeEqual(storedTokenBuffer, hashedTokenBuffer);
};
User.prototype.resetPassword = async function (newPassword) {
return this.update({
password: newPassword,
passwordResetToken: null,
passwordResetTokenExpiry: null,
// Increment JWT version to invalidate all existing sessions
jwtVersion: this.jwtVersion + 1,
});
};
module.exports = User;