This commit is contained in:
2025-07-20 20:43:06 +02:00
parent 0abee5b794
commit 29592c7fc8
93 changed files with 23400 additions and 131 deletions

420
backend/src/models/User.js Normal file
View File

@ -0,0 +1,420 @@
const bcrypt = require('bcrypt');
const crypto = require('crypto');
const dbConnection = require('../database/connection');
const { dbErrorHandler } = require('../middleware/errorHandler');
class User {
constructor(userData = {}) {
this.id = userData.id;
this.email = userData.email;
this.password_hash = userData.password_hash;
this.is_verified = userData.is_verified || false;
this.created_at = userData.created_at;
this.updated_at = userData.updated_at;
this.last_login = userData.last_login;
this.verification_token = userData.verification_token;
this.reset_token = userData.reset_token;
this.reset_expires = userData.reset_expires;
}
/**
* Hash a password using bcrypt
* @param {string} password - Plain text password
* @returns {Promise<string>} - Hashed password
*/
static async hashPassword(password) {
const saltRounds = 12;
return await bcrypt.hash(password, saltRounds);
}
/**
* Verify a password against a hash
* @param {string} password - Plain text password
* @param {string} hash - Hashed password
* @returns {Promise<boolean>} - True if password matches
*/
static async verifyPassword(password, hash) {
return await bcrypt.compare(password, hash);
}
/**
* Validate email format
* @param {string} email - Email address to validate
* @returns {boolean} - True if email format is valid
*/
static validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
/**
* Validate password strength
* @param {string} password - Password to validate
* @returns {object} - Validation result with isValid and errors
*/
static validatePassword(password) {
const errors = [];
if (!password || password.length < 8) {
errors.push('Password must be at least 8 characters long');
}
if (!/[A-Z]/.test(password)) {
errors.push('Password must contain at least one uppercase letter');
}
if (!/[a-z]/.test(password)) {
errors.push('Password must contain at least one lowercase letter');
}
if (!/\d/.test(password)) {
errors.push('Password must contain at least one number');
}
if (!/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) {
errors.push('Password must contain at least one special character');
}
return {
isValid: errors.length === 0,
errors
};
}
/**
* Generate a secure random token
* @returns {string} - Random token
*/
static generateToken() {
return crypto.randomBytes(32).toString('hex');
}
/**
* Create a new user
* @param {object} userData - User data
* @returns {Promise<User>} - Created user instance
*/
static async create(userData) {
const { email, password } = userData;
// Validate email format
if (!User.validateEmail(email)) {
throw new Error('Invalid email format');
}
// Validate password strength
const passwordValidation = User.validatePassword(password);
if (!passwordValidation.isValid) {
throw new Error(`Password validation failed: ${passwordValidation.errors.join(', ')}`);
}
// Check if user already exists
const existingUser = await User.findByEmail(email);
if (existingUser) {
throw new Error('User with this email already exists');
}
// Hash password
const password_hash = await User.hashPassword(password);
// Generate verification token
const verification_token = User.generateToken();
const query = `
INSERT INTO users (email, password_hash, verification_token)
VALUES ($1, $2, $3)
RETURNING *
`;
return await dbErrorHandler(async () => {
const result = await dbConnection.query(query, [email, password_hash, verification_token]);
return new User(result.rows[0]);
});
}
/**
* Find user by email
* @param {string} email - Email address
* @returns {Promise<User|null>} - User instance or null
*/
static async findByEmail(email) {
return await dbErrorHandler(async () => {
const query = 'SELECT * FROM users WHERE email = $1';
const result = await dbConnection.query(query, [email]);
if (result.rows.length === 0) {
return null;
}
return new User(result.rows[0]);
});
}
/**
* Find user by ID
* @param {string} id - User ID
* @returns {Promise<User|null>} - User instance or null
*/
static async findById(id) {
return await dbErrorHandler(async () => {
const query = 'SELECT * FROM users WHERE id = $1';
const result = await dbConnection.query(query, [id]);
if (result.rows.length === 0) {
return null;
}
return new User(result.rows[0]);
});
}
/**
* Find user by verification token
* @param {string} token - Verification token
* @returns {Promise<User|null>} - User instance or null
*/
static async findByVerificationToken(token) {
return await dbErrorHandler(async () => {
const query = 'SELECT * FROM users WHERE verification_token = $1';
const result = await dbConnection.query(query, [token]);
if (result.rows.length === 0) {
return null;
}
return new User(result.rows[0]);
});
}
/**
* Find user by reset token
* @param {string} token - Reset token
* @returns {Promise<User|null>} - User instance or null
*/
static async findByResetToken(token) {
return await dbErrorHandler(async () => {
const query = `
SELECT * FROM users
WHERE reset_token = $1 AND reset_expires > NOW()
`;
const result = await dbConnection.query(query, [token]);
if (result.rows.length === 0) {
return null;
}
return new User(result.rows[0]);
});
}
/**
* Verify user email
* @returns {Promise<boolean>} - True if verification successful
*/
async verifyEmail() {
return await dbErrorHandler(async () => {
const query = `
UPDATE users
SET is_verified = true, verification_token = NULL, updated_at = NOW()
WHERE id = $1
`;
const result = await dbConnection.query(query, [this.id]);
if (result.rowCount > 0) {
this.is_verified = true;
this.verification_token = null;
return true;
}
return false;
});
}
/**
* Update last login timestamp
* @returns {Promise<boolean>} - True if update successful
*/
async updateLastLogin() {
return await dbErrorHandler(async () => {
const query = `
UPDATE users
SET last_login = NOW(), updated_at = NOW()
WHERE id = $1
`;
const result = await dbConnection.query(query, [this.id]);
if (result.rowCount > 0) {
this.last_login = new Date();
return true;
}
return false;
});
}
/**
* Set password reset token
* @returns {Promise<string>} - Reset token
*/
async setResetToken() {
return await dbErrorHandler(async () => {
const reset_token = User.generateToken();
const reset_expires = new Date(Date.now() + 60 * 60 * 1000); // 1 hour from now
const query = `
UPDATE users
SET reset_token = $1, reset_expires = $2, updated_at = NOW()
WHERE id = $3
`;
await dbConnection.query(query, [reset_token, reset_expires, this.id]);
this.reset_token = reset_token;
this.reset_expires = reset_expires;
return reset_token;
});
}
/**
* Update password
* @param {string} newPassword - New password
* @returns {Promise<boolean>} - True if update successful
*/
async updatePassword(newPassword) {
// Validate new password
const passwordValidation = User.validatePassword(newPassword);
if (!passwordValidation.isValid) {
throw new Error(`Password validation failed: ${passwordValidation.errors.join(', ')}`);
}
return await dbErrorHandler(async () => {
const password_hash = await User.hashPassword(newPassword);
const query = `
UPDATE users
SET password_hash = $1, reset_token = NULL, reset_expires = NULL, updated_at = NOW()
WHERE id = $2
`;
const result = await dbConnection.query(query, [password_hash, this.id]);
if (result.rowCount > 0) {
this.password_hash = password_hash;
this.reset_token = null;
this.reset_expires = null;
return true;
}
return false;
});
}
/**
* Update user profile
* @param {object} updates - Fields to update
* @returns {Promise<boolean>} - True if update successful
*/
async update(updates) {
const allowedFields = ['email'];
const updateFields = [];
const values = [];
let paramIndex = 1;
for (const [field, value] of Object.entries(updates)) {
if (allowedFields.includes(field)) {
if (field === 'email' && !User.validateEmail(value)) {
throw new Error('Invalid email format');
}
updateFields.push(`${field} = $${paramIndex}`);
values.push(value);
paramIndex++;
}
}
if (updateFields.length === 0) {
return false;
}
updateFields.push(`updated_at = NOW()`);
values.push(this.id);
const query = `
UPDATE users
SET ${updateFields.join(', ')}
WHERE id = $${paramIndex}
`;
return await dbErrorHandler(async () => {
const result = await dbConnection.query(query, values);
if (result.rowCount > 0) {
// Update instance properties
for (const [field, value] of Object.entries(updates)) {
if (allowedFields.includes(field)) {
this[field] = value;
}
}
return true;
}
return false;
});
}
/**
* Delete user account
* @returns {Promise<boolean>} - True if deletion successful
*/
async delete() {
return await dbErrorHandler(async () => {
const query = 'DELETE FROM users WHERE id = $1';
const result = await dbConnection.query(query, [this.id]);
return result.rowCount > 0;
});
}
/**
* Get user data without sensitive information
* @returns {object} - Safe user data
*/
toSafeObject() {
return {
id: this.id,
email: this.email,
is_verified: this.is_verified,
created_at: this.created_at,
updated_at: this.updated_at,
last_login: this.last_login
};
}
/**
* Authenticate user with email and password
* @param {string} email - Email address
* @param {string} password - Password
* @returns {Promise<User|null>} - User instance if authentication successful
*/
static async authenticate(email, password) {
const user = await User.findByEmail(email);
if (!user) {
return null;
}
const isValidPassword = await User.verifyPassword(password, user.password_hash);
if (!isValidPassword) {
return null;
}
// Update last login
await user.updateLastLogin();
return user;
}
}
module.exports = User;