WIP
This commit is contained in:
434
backend/src/models/Bookmark.js
Normal file
434
backend/src/models/Bookmark.js
Normal file
@ -0,0 +1,434 @@
|
||||
const dbConnection = require('../database/connection');
|
||||
const { dbErrorHandler } = require('../middleware/errorHandler');
|
||||
|
||||
class Bookmark {
|
||||
constructor(bookmarkData = {}) {
|
||||
this.id = bookmarkData.id;
|
||||
this.user_id = bookmarkData.user_id;
|
||||
this.title = bookmarkData.title;
|
||||
this.url = bookmarkData.url;
|
||||
this.folder = bookmarkData.folder || '';
|
||||
this.add_date = bookmarkData.add_date;
|
||||
this.last_modified = bookmarkData.last_modified;
|
||||
this.icon = bookmarkData.icon;
|
||||
this.status = bookmarkData.status || 'unknown';
|
||||
this.created_at = bookmarkData.created_at;
|
||||
this.updated_at = bookmarkData.updated_at;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate bookmark data
|
||||
* @param {object} bookmarkData - Bookmark data to validate
|
||||
* @returns {object} - Validation result with isValid and errors
|
||||
*/
|
||||
static validateBookmark(bookmarkData) {
|
||||
const errors = [];
|
||||
|
||||
if (!bookmarkData.title || bookmarkData.title.trim().length === 0) {
|
||||
errors.push('Title is required');
|
||||
}
|
||||
|
||||
if (bookmarkData.title && bookmarkData.title.length > 500) {
|
||||
errors.push('Title must be 500 characters or less');
|
||||
}
|
||||
|
||||
if (!bookmarkData.url || bookmarkData.url.trim().length === 0) {
|
||||
errors.push('URL is required');
|
||||
}
|
||||
|
||||
if (bookmarkData.url) {
|
||||
try {
|
||||
new URL(bookmarkData.url);
|
||||
} catch (error) {
|
||||
errors.push('Invalid URL format');
|
||||
}
|
||||
}
|
||||
|
||||
if (bookmarkData.folder && bookmarkData.folder.length > 255) {
|
||||
errors.push('Folder name must be 255 characters or less');
|
||||
}
|
||||
|
||||
const validStatuses = ['unknown', 'valid', 'invalid', 'testing', 'duplicate'];
|
||||
if (bookmarkData.status && !validStatuses.includes(bookmarkData.status)) {
|
||||
errors.push('Invalid status value');
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new bookmark
|
||||
* @param {string} userId - User ID
|
||||
* @param {object} bookmarkData - Bookmark data
|
||||
* @returns {Promise<Bookmark>} - Created bookmark instance
|
||||
*/
|
||||
static async create(userId, bookmarkData) {
|
||||
// Validate bookmark data
|
||||
const validation = Bookmark.validateBookmark(bookmarkData);
|
||||
if (!validation.isValid) {
|
||||
throw new Error(`Bookmark validation failed: ${validation.errors.join(', ')}`);
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
url,
|
||||
folder = '',
|
||||
add_date = new Date(),
|
||||
last_modified,
|
||||
icon,
|
||||
status = 'unknown'
|
||||
} = bookmarkData;
|
||||
|
||||
const query = `
|
||||
INSERT INTO bookmarks (user_id, title, url, folder, add_date, last_modified, icon, status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const values = [
|
||||
userId,
|
||||
title.trim(),
|
||||
url.trim(),
|
||||
folder.trim(),
|
||||
add_date,
|
||||
last_modified,
|
||||
icon,
|
||||
status
|
||||
];
|
||||
|
||||
return await dbErrorHandler(async () => {
|
||||
const result = await dbConnection.query(query, values);
|
||||
return new Bookmark(result.rows[0]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find bookmarks by user ID with pagination and filtering
|
||||
* @param {string} userId - User ID
|
||||
* @param {object} options - Query options
|
||||
* @returns {Promise<object>} - Bookmarks with pagination info
|
||||
*/
|
||||
static async findByUserId(userId, options = {}) {
|
||||
return await dbErrorHandler(async () => {
|
||||
const {
|
||||
page = 1,
|
||||
limit = null,
|
||||
folder,
|
||||
status,
|
||||
search,
|
||||
sortBy = 'add_date',
|
||||
sortOrder = 'DESC'
|
||||
} = options;
|
||||
|
||||
const offset = limit ? (page - 1) * limit : 0;
|
||||
const validSortColumns = ['add_date', 'title', 'url', 'folder', 'created_at', 'updated_at'];
|
||||
const validSortOrders = ['ASC', 'DESC'];
|
||||
|
||||
const sortColumn = validSortColumns.includes(sortBy) ? sortBy : 'add_date';
|
||||
const sortDirection = validSortOrders.includes(sortOrder.toUpperCase()) ? sortOrder.toUpperCase() : 'DESC';
|
||||
|
||||
let whereConditions = ['user_id = $1'];
|
||||
let queryParams = [userId];
|
||||
let paramIndex = 2;
|
||||
|
||||
// Add folder filter
|
||||
if (folder !== undefined) {
|
||||
whereConditions.push(`folder = $${paramIndex}`);
|
||||
queryParams.push(folder);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// Add status filter
|
||||
if (status) {
|
||||
whereConditions.push(`status = $${paramIndex}`);
|
||||
queryParams.push(status);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// Add search filter
|
||||
if (search) {
|
||||
whereConditions.push(`(title ILIKE $${paramIndex} OR url ILIKE $${paramIndex})`);
|
||||
queryParams.push(`%${search}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const whereClause = whereConditions.join(' AND ');
|
||||
|
||||
// Get total count
|
||||
const countQuery = `SELECT COUNT(*) FROM bookmarks WHERE ${whereClause}`;
|
||||
const countResult = await dbConnection.query(countQuery, queryParams);
|
||||
const totalCount = parseInt(countResult.rows[0].count);
|
||||
|
||||
// Build query with optional LIMIT and OFFSET
|
||||
let query = `
|
||||
SELECT * FROM bookmarks
|
||||
WHERE ${whereClause}
|
||||
ORDER BY ${sortColumn} ${sortDirection}
|
||||
`;
|
||||
|
||||
// Only add LIMIT and OFFSET if limit is specified
|
||||
if (limit !== null && limit > 0) {
|
||||
query += ` LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
|
||||
queryParams.push(limit, offset);
|
||||
}
|
||||
|
||||
const result = await dbConnection.query(query, queryParams);
|
||||
const bookmarks = result.rows.map(row => new Bookmark(row));
|
||||
|
||||
// Calculate pagination info
|
||||
const totalPages = limit ? Math.ceil(totalCount / limit) : 1;
|
||||
|
||||
return {
|
||||
bookmarks,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
totalCount,
|
||||
totalPages,
|
||||
hasNext: limit ? page < totalPages : false,
|
||||
hasPrev: page > 1
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find bookmark by ID and user ID (for ownership validation)
|
||||
* @param {string} bookmarkId - Bookmark ID
|
||||
* @param {string} userId - User ID
|
||||
* @returns {Promise<Bookmark|null>} - Bookmark instance or null
|
||||
*/
|
||||
static async findByIdAndUserId(bookmarkId, userId) {
|
||||
return await dbErrorHandler(async () => {
|
||||
const query = 'SELECT * FROM bookmarks WHERE id = $1 AND user_id = $2';
|
||||
const result = await dbConnection.query(query, [bookmarkId, userId]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Bookmark(result.rows[0]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update bookmark
|
||||
* @param {object} updates - Fields to update
|
||||
* @returns {Promise<boolean>} - True if update successful
|
||||
*/
|
||||
async update(updates) {
|
||||
const allowedFields = ['title', 'url', 'folder', 'last_modified', 'icon', 'status'];
|
||||
const updateFields = [];
|
||||
const values = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// Validate updates
|
||||
if (Object.keys(updates).length > 0) {
|
||||
const validation = Bookmark.validateBookmark({ ...this.toObject(), ...updates });
|
||||
if (!validation.isValid) {
|
||||
throw new Error(`Bookmark validation failed: ${validation.errors.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [field, value] of Object.entries(updates)) {
|
||||
if (allowedFields.includes(field)) {
|
||||
updateFields.push(`${field} = $${paramIndex}`);
|
||||
values.push(field === 'title' || field === 'url' || field === 'folder' ? value.trim() : value);
|
||||
paramIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
if (updateFields.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
updateFields.push(`updated_at = CURRENT_TIMESTAMP`);
|
||||
values.push(this.id, this.user_id);
|
||||
|
||||
const query = `
|
||||
UPDATE bookmarks
|
||||
SET ${updateFields.join(', ')}
|
||||
WHERE id = $${paramIndex} AND user_id = $${paramIndex + 1}
|
||||
`;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
this.updated_at = new Date();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete bookmark
|
||||
* @returns {Promise<boolean>} - True if deletion successful
|
||||
*/
|
||||
async delete() {
|
||||
return await dbErrorHandler(async () => {
|
||||
const query = 'DELETE FROM bookmarks WHERE id = $1 AND user_id = $2';
|
||||
const result = await dbConnection.query(query, [this.id, this.user_id]);
|
||||
return result.rowCount > 0;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Get all folders for a user
|
||||
* @param {string} userId - User ID
|
||||
* @returns {Promise<Array>} - Array of folder names with counts
|
||||
*/
|
||||
static async getFoldersByUserId(userId) {
|
||||
return await dbErrorHandler(async () => {
|
||||
const query = `
|
||||
SELECT folder, COUNT(*) as count
|
||||
FROM bookmarks
|
||||
WHERE user_id = $1
|
||||
GROUP BY folder
|
||||
ORDER BY folder
|
||||
`;
|
||||
|
||||
const result = await dbConnection.query(query, [userId]);
|
||||
return result.rows;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bookmark statistics for a user
|
||||
* @param {string} userId - User ID
|
||||
* @returns {Promise<object>} - Statistics object
|
||||
*/
|
||||
static async getStatsByUserId(userId) {
|
||||
return await dbErrorHandler(async () => {
|
||||
const query = `
|
||||
SELECT
|
||||
COUNT(*) as total_bookmarks,
|
||||
COUNT(DISTINCT folder) as total_folders,
|
||||
COUNT(CASE WHEN status = 'valid' THEN 1 END) as valid_bookmarks,
|
||||
COUNT(CASE WHEN status = 'invalid' THEN 1 END) as invalid_bookmarks,
|
||||
COUNT(CASE WHEN status = 'duplicate' THEN 1 END) as duplicate_bookmarks,
|
||||
COUNT(CASE WHEN status = 'unknown' THEN 1 END) as unknown_bookmarks
|
||||
FROM bookmarks
|
||||
WHERE user_id = $1
|
||||
`;
|
||||
|
||||
const result = await dbConnection.query(query, [userId]);
|
||||
return result.rows[0];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk create bookmarks (for import functionality)
|
||||
* @param {string} userId - User ID
|
||||
* @param {Array} bookmarksData - Array of bookmark data
|
||||
* @returns {Promise<Array>} - Array of created bookmarks
|
||||
*/
|
||||
static async bulkCreate(userId, bookmarksData) {
|
||||
if (!Array.isArray(bookmarksData) || bookmarksData.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Validate all bookmarks first
|
||||
for (const bookmarkData of bookmarksData) {
|
||||
const validation = Bookmark.validateBookmark(bookmarkData);
|
||||
if (!validation.isValid) {
|
||||
throw new Error(`Bookmark validation failed: ${validation.errors.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
return await dbErrorHandler(async () => {
|
||||
const values = [];
|
||||
const placeholders = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
for (const bookmarkData of bookmarksData) {
|
||||
const {
|
||||
title,
|
||||
url,
|
||||
folder = '',
|
||||
add_date = new Date(),
|
||||
last_modified,
|
||||
icon,
|
||||
status = 'unknown'
|
||||
} = bookmarkData;
|
||||
|
||||
placeholders.push(`($${paramIndex}, $${paramIndex + 1}, $${paramIndex + 2}, $${paramIndex + 3}, $${paramIndex + 4}, $${paramIndex + 5}, $${paramIndex + 6}, $${paramIndex + 7})`);
|
||||
values.push(userId, title.trim(), url.trim(), folder.trim(), add_date, last_modified, icon, status);
|
||||
paramIndex += 8;
|
||||
}
|
||||
|
||||
const query = `
|
||||
INSERT INTO bookmarks (user_id, title, url, folder, add_date, last_modified, icon, status)
|
||||
VALUES ${placeholders.join(', ')}
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await dbConnection.query(query, values);
|
||||
return result.rows.map(row => new Bookmark(row));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all bookmarks for a user
|
||||
* @param {string} userId - User ID
|
||||
* @returns {Promise<number>} - Number of deleted bookmarks
|
||||
*/
|
||||
static async deleteAllByUserId(userId) {
|
||||
return await dbErrorHandler(async () => {
|
||||
const query = 'DELETE FROM bookmarks WHERE user_id = $1';
|
||||
const result = await dbConnection.query(query, [userId]);
|
||||
return result.rowCount;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert bookmark to plain object
|
||||
* @returns {object} - Plain object representation
|
||||
*/
|
||||
toObject() {
|
||||
return {
|
||||
id: this.id,
|
||||
user_id: this.user_id,
|
||||
title: this.title,
|
||||
url: this.url,
|
||||
folder: this.folder,
|
||||
add_date: this.add_date,
|
||||
last_modified: this.last_modified,
|
||||
icon: this.icon,
|
||||
status: this.status,
|
||||
created_at: this.created_at,
|
||||
updated_at: this.updated_at
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert bookmark to safe object (without user_id for API responses)
|
||||
* @returns {object} - Safe object representation
|
||||
*/
|
||||
toSafeObject() {
|
||||
return {
|
||||
id: this.id,
|
||||
title: this.title,
|
||||
url: this.url,
|
||||
folder: this.folder,
|
||||
add_date: this.add_date,
|
||||
last_modified: this.last_modified,
|
||||
icon: this.icon,
|
||||
status: this.status,
|
||||
created_at: this.created_at,
|
||||
updated_at: this.updated_at
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Bookmark;
|
||||
420
backend/src/models/User.js
Normal file
420
backend/src/models/User.js
Normal 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;
|
||||
Reference in New Issue
Block a user