diff --git a/backend/config/database.js b/backend/config/database.js index c35687d..56b1f3f 100644 --- a/backend/config/database.js +++ b/backend/config/database.js @@ -55,7 +55,13 @@ pool.on('remove', (client) => { }); /** - * Initialize database connection and create tables + * Initialize database connection and create necessary tables. + * + * This function establishes a connection to the database using the provided configuration, + * tests the connection by executing a simple query, and logs the connection details. + * If the connection is successful, it proceeds to create the required tables by calling + * the createTables function. In case of any errors during the process, it logs the error + * and rethrows it for further handling. */ export async function initializeDatabase() { try { @@ -84,7 +90,12 @@ export async function initializeDatabase() { } /** - * Create database tables + * Create database tables and initialize the database schema. + * + * This function connects to the database, begins a transaction, and creates several tables including users, wheel_stages, user_progress, user_sessions, user_encrypted_data, analytics_events, and audit_logs. It also enables necessary extensions, creates indexes for performance, and sets up triggers for updating timestamps. Finally, it inserts default wheel stages if they do not already exist. If any error occurs, the transaction is rolled back. + * + * @returns {Promise} A promise that resolves when the tables are created and initialized. + * @throws {Error} If there is an error during the database operations. */ async function createTables() { const client = await pool.connect(); @@ -302,7 +313,12 @@ async function createTables() { } /** - * Insert default wheel stages + * Insert default wheel stages into the database if none exist. + * + * This asynchronous function connects to the database and checks if any wheel stages are already present. + * If no stages are found, it inserts a predefined set of default stages, each with attributes such as title, + * symbol, essence, meaning, action, chant, and order_index. The function also logs the insertion process + * and handles any potential errors during the database operations. */ async function insertDefaultWheelStages() { const defaultStages = [ @@ -425,7 +441,14 @@ async function insertDefaultWheelStages() { } /** - * Execute a database query with error handling and logging + * Execute a database query with error handling and logging. + * + * This function connects to the database, executes the provided SQL query with optional parameters, + * and logs the duration and result of the query. In case of an error, it logs the error message and + * rethrows the error. The database client is released after the operation, ensuring proper resource management. + * + * @param {string} text - The SQL query to be executed. + * @param {Array} [params=[]] - The parameters for the SQL query. */ export async function query(text, params = []) { const start = Date.now(); @@ -454,7 +477,15 @@ export async function query(text, params = []) { } /** - * Execute a transaction + * Execute a transaction. + * + * This function establishes a connection to the database, begins a transaction, + * and executes the provided callback function with the database client. If the + * callback completes successfully, the transaction is committed; if an error + * occurs, the transaction is rolled back. Finally, the database client is released. + * + * @param {Function} callback - A function that takes the database client as an argument + * and performs operations within the transaction. */ export async function transaction(callback) { const client = await pool.connect(); @@ -473,7 +504,7 @@ export async function transaction(callback) { } /** - * Store encrypted data for a user + * Store encrypted data for a user in the database. */ export async function storeEncryptedData(userId, dataType, data) { const encryptedData = encryptField(data); @@ -487,7 +518,7 @@ export async function storeEncryptedData(userId, dataType, data) { } /** - * Retrieve encrypted data for a user + * Retrieve and decrypt encrypted data for a user. */ export async function getEncryptedData(userId, dataType) { const result = await query(` @@ -504,7 +535,7 @@ export async function getEncryptedData(userId, dataType) { } /** - * Close database connection + * Closes the database connection. */ export async function closeDatabase() { try { @@ -516,7 +547,12 @@ export async function closeDatabase() { } /** - * Health check + * Performs a health check on the database. + * + * This function executes a simple query to verify the database's availability. + * It checks if the result indicates a healthy state by comparing the returned value + * to 1. In case of an error during the query execution, it logs the error and + * returns false to indicate an unhealthy state. */ export async function healthCheck() { try { diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js index 8190f78..9f416dc 100644 --- a/backend/middleware/auth.js +++ b/backend/middleware/auth.js @@ -16,7 +16,7 @@ const JWT_EXPIRY = process.env.JWT_EXPIRY || '15m'; const JWT_REFRESH_EXPIRY = process.env.JWT_REFRESH_EXPIRY || '7d'; /** - * Generate JWT access token + * Generates a JWT access token with the given payload. */ export function generateAccessToken(payload) { return jwt.sign( @@ -37,7 +37,7 @@ export function generateAccessToken(payload) { } /** - * Generate JWT refresh token + * Generates a JWT refresh token. */ export function generateRefreshToken(payload) { return jwt.sign( @@ -58,7 +58,14 @@ export function generateRefreshToken(payload) { } /** - * Verify JWT token + * Verify JWT token. + * + * This function checks the validity of a JWT token using a specified secret based on whether it is a refresh token or not. + * It decodes the token and returns an object indicating its validity, the decoded payload, and whether it has expired. + * If an error occurs during verification, it handles different error types to provide specific feedback on the token's status. + * + * @param {string} token - The JWT token to verify. + * @param {boolean} [isRefresh=false] - Indicates if the token is a refresh token. */ export function verifyToken(token, isRefresh = false) { try { @@ -103,7 +110,17 @@ export function verifyToken(token, isRefresh = false) { } /** - * Authentication middleware + * Authentication middleware for validating user tokens. + * + * This middleware checks for a valid Bearer token in the authorization header, verifies its validity and type, + * and retrieves user information. It handles various authentication errors, including blacklisted tokens, + * expired tokens, and inactive accounts, responding with appropriate status codes and messages. + * If successful, it attaches user and token information to the request object for further processing. + * + * @param req - The request object containing the HTTP request data. + * @param res - The response object used to send HTTP responses. + * @param next - The next middleware function in the stack. + * @throws Error If an internal error occurs during authentication. */ export async function authMiddleware(req, res, next) { try { @@ -212,7 +229,9 @@ export async function authMiddleware(req, res, next) { } /** - * Optional authentication middleware (doesn't fail if no token) + * Optional authentication middleware that allows requests to proceed without a token. + * + * This middleware checks for the presence of an authorization header. If the header is missing or does not start with 'Bearer ', it sets req.user and req.token to null and calls next() to continue the request. If the header is present, it attempts to call the authMiddleware function. If authMiddleware throws an error, it catches the error, sets req.user and req.token to null, and continues the request. */ export async function optionalAuthMiddleware(req, res, next) { const authHeader = req.headers.authorization; @@ -234,7 +253,14 @@ export async function optionalAuthMiddleware(req, res, next) { } /** - * Role-based authorization middleware + * Role-based authorization middleware. + * + * This middleware checks if the user is authenticated and has one of the required roles. + * If the user is not authenticated, a 401 status is returned with an appropriate message. + * If the user's role is not included in the specified roles, a 403 status is returned, + * indicating insufficient permissions. If both checks pass, the middleware calls the next function. + * + * @param {...string} roles - The roles that are required to access the resource. */ export function requireRole(...roles) { return (req, res, next) => { @@ -259,7 +285,16 @@ export function requireRole(...roles) { } /** - * Refresh token middleware + * Middleware to refresh the token for authenticated users. + * + * This function checks for the presence of a refresh token in the request body, verifies its validity, and ensures it is not blacklisted. + * It also checks the token type and retrieves the associated user information, attaching it to the request object for further processing. + * If any validation fails, appropriate error responses are sent back to the client. + * + * @param req - The request object containing the refresh token in the body. + * @param res - The response object used to send responses back to the client. + * @param next - The next middleware function in the stack. + * @throws Error If an internal server error occurs during the token refresh process. */ export async function refreshTokenMiddleware(req, res, next) { try { @@ -334,7 +369,16 @@ export async function refreshTokenMiddleware(req, res, next) { } /** - * Logout middleware - blacklist tokens + * Logout middleware - blacklist tokens. + * + * This middleware handles the logout process by blacklisting the access token and, if provided, the refresh token. + * It checks for the presence of the access token and blacklists it if available. If a refresh token is included in + * the request body, it verifies the token and blacklists it if valid. The function logs the logout action and + * proceeds to the next middleware, even if an error occurs during the blacklisting process. + * + * @param {Object} req - The request object containing user and token information. + * @param {Object} res - The response object. + * @param {Function} next - The next middleware function to call. */ export async function logoutMiddleware(req, res, next) { try { @@ -367,7 +411,7 @@ export async function logoutMiddleware(req, res, next) { } /** - * Generate token pair (access + refresh) + * Generates a token pair (access + refresh) from the given payload. */ export function generateTokenPair(payload) { const accessToken = generateAccessToken(payload); @@ -383,7 +427,14 @@ export function generateTokenPair(payload) { } /** - * Extract token from request + * Extract token from request. + * + * This function retrieves a token from the provided request object. It first checks the + * authorization header for a Bearer token and returns it if present. If the header is + * not available or does not contain a Bearer token, it falls back to checking the query + * parameters for a token, specifically for WebSocket scenarios. + * + * @param {Object} req - The request object containing headers and query parameters. */ export function extractTokenFromRequest(req) { const authHeader = req.headers.authorization; diff --git a/backend/models/User.js b/backend/models/User.js index 04f0f93..0e6c88e 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -9,7 +9,22 @@ import logger from '../utils/logger.js'; import crypto from 'crypto'; /** - * Create a new user + * Create a new user. + * + * This function takes user data, including username, email, and password, and inserts a new user record into the database. + * It handles default values for isActive, emailVerified, and role. After successfully creating the user, it logs the creation + * event and returns the newly created user's information. In case of an error, it logs the error and rethrows it. + * + * @param {Object} userData - The data for the new user. + * @param {string} userData.username - The username of the new user. + * @param {string} userData.email - The email address of the new user. + * @param {string} userData.password - The password for the new user. + * @param {string} userData.firstName - The first name of the new user. + * @param {string} userData.lastName - The last name of the new user. + * @param {string} userData.encryptionSalt - The salt used for password encryption. + * @param {boolean} [userData.isActive=true] - Indicates if the user is active. + * @param {boolean} [userData.emailVerified=false] - Indicates if the user's email is verified. + * @param {string} [userData.role='user'] - The role assigned to the new user. */ export async function createUser(userData) { try { @@ -50,7 +65,17 @@ export async function createUser(userData) { } /** - * Get user by ID + * Get user details by their ID. + * + * This function retrieves user information from the database based on the provided userId. + * It allows for an optional inclusion of the user's password hash. The retrieved data is then + * transformed from snake_case to camelCase for consistency in the API response. If no user is found, + * it returns null. Any errors during the query process are logged and rethrown. + * + * @param userId - The ID of the user to retrieve. + * @param includePassword - A boolean indicating whether to include the user's password hash in the response. + * @returns An object containing user details in camelCase format, or null if no user is found. + * @throws Error If there is an issue with the database query. */ export async function getUserById(userId, includePassword = false) { try { @@ -94,7 +119,14 @@ export async function getUserById(userId, includePassword = false) { } /** - * Get user by email + * Get user details by their email address. + * + * This function queries the database for a user with the specified email. It allows for the inclusion of the user's password hash based on the includePassword parameter. If no user is found, it returns null. The function also handles errors by logging them and rethrowing the error for further handling. + * + * @param email - The email address of the user to retrieve. + * @param includePassword - A boolean indicating whether to include the user's password hash in the returned object. + * @returns An object containing user details, or null if no user is found. + * @throws Error If there is an issue querying the database. */ export async function getUserByEmail(email, includePassword = false) { try { @@ -137,7 +169,11 @@ export async function getUserByEmail(email, includePassword = false) { } /** - * Get user by username + * Get user by username. + * + * This function retrieves a user from the database based on the provided username. It executes a SQL query to fetch user details, including id, email, and role. If no user is found, it returns null. In case of an error during the query execution, it logs the error and rethrows it for further handling. + * + * @param {string} username - The username of the user to retrieve. */ export async function getUserByUsername(username) { try { @@ -171,7 +207,7 @@ export async function getUserByUsername(username) { } /** - * Update user last login timestamp + * Update the last login timestamp for a user. */ export async function updateUserLastLogin(userId) { try { @@ -187,7 +223,7 @@ export async function updateUserLastLogin(userId) { } /** - * Update user last seen timestamp + * Update user last seen timestamp in the database. */ export async function updateUserLastSeen(userId) { try { @@ -201,7 +237,13 @@ export async function updateUserLastSeen(userId) { } /** - * Update user password and encryption salt + * Update user password and encryption salt. + * + * This function updates the user's password hash and encryption salt in the database. It also clears all encrypted user data, as the previous data becomes unreadable with the new salt. Additionally, it invalidates all active user sessions to ensure security. The operation is performed within a transaction to maintain data integrity, and any errors during the process are logged for auditing purposes. + * + * @param {string} userId - The ID of the user whose password is being updated. + * @param {string} newPasswordHash - The new password hash to be set for the user. + * @param {string} newEncryptionSalt - The new encryption salt to be set for the user. */ export async function updateUserPassword(userId, newPasswordHash, newEncryptionSalt) { try { @@ -232,7 +274,20 @@ export async function updateUserPassword(userId, newPasswordHash, newEncryptionS } /** - * Update user profile + * Update user profile. + * + * This function updates the user's profile information in the database based on the provided userId and profileData. + * It uses a SQL query to update fields such as first name, last name, bio, avatar URL, and preferences, + * while ensuring that only non-null values are updated. If the user is not found, an error is thrown. + * Additionally, it logs the update action and returns the updated user information. + * + * @param {string} userId - The ID of the user whose profile is to be updated. + * @param {Object} profileData - The new profile data for the user. + * @param {string} profileData.firstName - The user's first name. + * @param {string} profileData.lastName - The user's last name. + * @param {string} profileData.bio - The user's biography. + * @param {string} profileData.avatarUrl - The URL of the user's avatar. + * @param {Object} profileData.preferences - The user's preferences. */ export async function updateUserProfile(userId, profileData) { try { @@ -291,7 +346,15 @@ export async function updateUserProfile(userId, profileData) { } /** - * Create password reset token + * Create a password reset token for a user. + * + * This function updates the user's record in the database with a new password reset token and its expiration time. + * It logs an audit message upon successful creation and handles any errors that occur during the database update, + * logging the error details before rethrowing the error. + * + * @param {string} userId - The ID of the user for whom the password reset token is being created. + * @param {string} token - The password reset token to be set for the user. + * @param {Date} expiresAt - The expiration date and time for the password reset token. */ export async function createPasswordResetToken(userId, token, expiresAt) { try { @@ -309,7 +372,14 @@ export async function createPasswordResetToken(userId, token, expiresAt) { } /** - * Validate password reset token and return user + * Validate the password reset token and return user information. + * + * This function queries the database to check if the provided password reset token is valid + * and has not expired. It retrieves the user's details if the token is valid and the user is active. + * If the token is invalid or expired, it returns null. In case of an error during the query, + * it logs the error and rethrows it. + * + * @param {string} token - The password reset token to validate. */ export async function validatePasswordResetToken(token) { try { @@ -341,7 +411,23 @@ export async function validatePasswordResetToken(token) { } /** - * Get users with pagination + * Get users with pagination and filtering options. + * + * This function retrieves a paginated list of users from the database based on the provided options. + * It constructs a dynamic SQL query with filters for search, role, and active status, and returns + * the user data along with pagination information such as total count, total pages, and current page. + * + * @param options - An object containing pagination and filtering options. + * @param options.page - The page number to retrieve (default is 1). + * @param options.limit - The number of users per page (default is 20). + * @param options.sortBy - The field to sort by (default is 'created_at'). + * @param options.sortOrder - The order of sorting (default is 'desc'). + * @param options.search - A search term to filter users by username or email. + * @param options.role - A specific role to filter users. + * @param options.isActive - A boolean to filter users by active status. + * @returns An object containing the list of users, total count, total pages, current page, + * and flags indicating if there are next or previous pages. + * @throws Error If the query fails to execute. */ export async function getUsers(options = {}) { try { @@ -427,7 +513,13 @@ export async function getUsers(options = {}) { } /** - * Delete user (soft delete by deactivating) + * Delete user (soft delete by deactivating). + * + * This function performs a soft delete of a user by deactivating their account and updating their email and username + * to indicate deletion. It also invalidates all active sessions associated with the user. The function is wrapped in a + * transaction to ensure atomicity. In case of an error, it logs the failure and rethrows the error for further handling. + * + * @param {string} userId - The ID of the user to be deleted. */ export async function deleteUser(userId) { try { @@ -458,7 +550,15 @@ export async function deleteUser(userId) { } /** - * Store encrypted sensitive data for user + * Store encrypted sensitive data for user. + * + * This function encrypts the provided data using the encryptField function and stores it in the user_encrypted_data table. + * If a record for the userId and dataType already exists, it updates the encrypted_data and the updated_at timestamp. + * The operation is wrapped in a try-catch block to handle any errors that may occur during the database operation. + * + * @param {string} userId - The unique identifier for the user. + * @param {string} dataType - The type of data being stored. + * @param {any} data - The sensitive data to be encrypted and stored. */ export async function storeUserEncryptedData(userId, dataType, data) { try { @@ -482,7 +582,14 @@ export async function storeUserEncryptedData(userId, dataType, data) { } /** - * Retrieve encrypted sensitive data for user + * Retrieve decrypted sensitive data for a user. + * + * This function queries the database for encrypted data associated with a specific userId and dataType. + * If no data is found, it returns null. Otherwise, it decrypts the retrieved encrypted data using the + * decryptField function and returns the decrypted result. Errors during the process are logged for debugging. + * + * @param {string} userId - The ID of the user whose data is being retrieved. + * @param {string} dataType - The type of data to retrieve for the user. */ export async function getUserEncryptedData(userId, dataType) { try { @@ -506,7 +613,11 @@ export async function getUserEncryptedData(userId, dataType) { } /** - * Get user statistics + * Get user statistics for a specific user. + * + * This function retrieves various statistics related to a user's progress, including the number of stages completed, total time spent, total sessions, average rating, and the timestamp of the last session. It executes a SQL query to gather this data from the user_progress table, filtering by the provided userId. If an error occurs during the query execution, it logs the error and rethrows it. + * + * @param {number} userId - The ID of the user for whom to retrieve statistics. */ export async function getUserStats(userId) { try { diff --git a/backend/server.js b/backend/server.js index 7825e24..b7ea44c 100644 --- a/backend/server.js +++ b/backend/server.js @@ -302,6 +302,16 @@ app.use(errorHandler); process.on('SIGTERM', gracefulShutdown); process.on('SIGINT', gracefulShutdown); +/** + * Initiates a graceful shutdown of the server upon receiving a signal. + * + * The function logs the received signal and attempts to close the HTTP server. + * It then proceeds to close database and Redis connections, logging each step. + * If the shutdown process takes longer than 30 seconds, it forcefully exits the process. + * In case of any errors during the shutdown, it logs the error and exits with a failure status. + * + * @param {string} signal - The signal that triggered the shutdown process. + */ async function gracefulShutdown(signal) { logger.info(`Received ${signal}. Starting graceful shutdown...`); @@ -334,6 +344,9 @@ async function gracefulShutdown(signal) { // === HELPER FUNCTIONS === +/** + * Retrieves the stages of the wheel, typically from a database. + */ async function getWheelStages() { // This would typically come from database return [ @@ -350,21 +363,31 @@ async function getWheelStages() { ]; } +/** + * Records the progress data for a user. + */ async function recordProgress(progressData) { // This would save to database logger.info(`Recording progress for user ${progressData.userId}, stage ${progressData.stageId}`); return progressData; } +/** Encrypts insights using AES-GCM encryption. */ async function encryptInsights(insights) { // This would use AES-GCM encryption return insights; // Placeholder } +/** + * Closes database connections. + */ async function closeDatabase() { // Close database connections } +/** + * Closes Redis connections. + */ async function closeRedis() { // Close Redis connections }