From d06734dedb52b4e14f801d1a84ddc8ca2746cb82 Mon Sep 17 00:00:00 2001 From: Ahmad Shah Date: Mon, 13 Oct 2025 15:13:56 +0500 Subject: [PATCH 1/4] Refactor database configuration, add error handling classes, and implement utility functions for data manipulation and validation --- src/config/database.config.js | 35 +++- src/enums/index.js | 51 ++++++ src/enums/user.enum.js | 12 -- src/errors/error-classes.js | 60 ------- .../authenticate-account.middleware.js | 10 +- src/middleware/error-handling.middleware.js | 21 ++- src/models/index.js | 39 +++-- src/schemas/class.schema.js | 9 - src/schemas/institute.schema.js | 59 ------- src/schemas/student.schema.js | 155 ------------------ src/schemas/transport.schema.js | 10 -- src/utils/case.util.js | 52 ++++++ src/utils/errors/error-classes.js | 86 ++++++++++ src/utils/hash-password.util.js | 14 ++ src/utils/isValidKsuid.util.js | 15 ++ src/utils/json.util.js | 47 ++++++ src/utils/jwt.util.js | 22 +-- src/utils/response.util.js | 73 +++++++++ src/utils/sanitizeData.util.js | 14 ++ src/utils/sequelize-filter.util.js | 119 ++++++++++++++ src/utils/validation.util.js | 21 +++ 21 files changed, 568 insertions(+), 356 deletions(-) create mode 100644 src/enums/index.js delete mode 100644 src/enums/user.enum.js delete mode 100644 src/errors/error-classes.js delete mode 100644 src/schemas/class.schema.js delete mode 100644 src/schemas/institute.schema.js delete mode 100644 src/schemas/student.schema.js delete mode 100644 src/schemas/transport.schema.js create mode 100644 src/utils/case.util.js create mode 100644 src/utils/errors/error-classes.js create mode 100644 src/utils/hash-password.util.js create mode 100644 src/utils/isValidKsuid.util.js create mode 100644 src/utils/json.util.js create mode 100644 src/utils/response.util.js create mode 100644 src/utils/sanitizeData.util.js create mode 100644 src/utils/sequelize-filter.util.js create mode 100644 src/utils/validation.util.js diff --git a/src/config/database.config.js b/src/config/database.config.js index a61bbc7..2ab637f 100644 --- a/src/config/database.config.js +++ b/src/config/database.config.js @@ -2,11 +2,36 @@ const dotenv = require("dotenv"); dotenv.config(); -module.exports = { +const databaseConfig = { host: process.env.DB_HOST || "localhost", - port: process.env.DB_PORT || 5432, + port: parseInt(process.env.DB_PORT) || 5432, username: process.env.DB_USER || "postgres", - password: process.env.DB_PASSWORD || "12345678", - database: process.env.DB_NAME || "medpost", - dialect: "postgres", + password: process.env.DB_PASSWORD || "postgres", + database: process.env.DB_NAME || "app_db", + dialect: "postgresql", + logging: process.env.NODE_ENV === "development" ? console.log : false, + dialectOptions: + process.env.DB_HOST !== "localhost" && process.env.DB_HOST !== "127.0.0.1" + ? { + ssl: { + require: true, + rejectUnauthorized: false, + }, + channel_binding: "require", + statement_timeout: 60000, // 60 seconds + connectionTimeoutMillis: 60000, // 60 seconds + } + : { + statement_timeout: 60000, // 60 seconds + connectionTimeoutMillis: 60000, // 60 seconds + }, + pool: { + max: 5, + min: 0, + idle: 10000, + acquire: 10000, + evict: 1000, + }, }; + +module.exports = databaseConfig; diff --git a/src/enums/index.js b/src/enums/index.js new file mode 100644 index 0000000..ab0bb09 --- /dev/null +++ b/src/enums/index.js @@ -0,0 +1,51 @@ +const GENDERS = Object.freeze({ + MALE: "MALE", + FEMALE: "FEMALE", + NOT_SPECIFIED: "NOT SPECIFIED", +}); + +const ERROR_NAMES = Object.freeze({ + BASE_ERROR: "BaseError", + DATABASE_CONNECTION_ERROR: "DatabaseConnectionError", + VALIDATION_ERROR: "ValidationError", + AUTHENTICATION_ERROR: "AuthenticationError", + NOT_FOUND_ERROR: "NotFoundError", + DUPLICATE_ENTRY_ERROR: "DuplicateEntryError", + UNAUTHORIZED_ERROR: "UnauthorizedError", + USER_EXISTS_ERROR: "UserExistsError", + UNIQUE_CONSTRAINT_ERROR: "UniqueConstraintsError", + FILE_UPLOAD_ERROR: "FileUploadError", + FILE_DOWNLOAD_ERROR: "FileDownloadError", +}); + +const ERROR_CODES = Object.freeze({ + INTERNAL_ERROR: "INTERNAL_ERROR", + VALIDATION_ERROR: "VALIDATION_ERROR", + DATABASE_CONNECTION_ERROR: "DATABASE_CONNECTION_ERROR", + AUTHENTICATION_ERROR: "AUTHENTICATION_ERROR", + UNAUTHORIZED_ERROR: "UNAUTHORIZED_ERROR", + NOT_FOUND_ERROR: "NOT_FOUND_ERROR", + DUPLICATE_ENTRY_ERROR: "DUPLICATE_ENTRY_ERROR", + UNIQUE_CONSTRAINTS_ERROR: "UNIQUE_CONSTRAINTS_ERROR", + USER_EXISTS_ERROR: "USER_EXISTS_ERROR", + FILE_UPLOAD_ERROR: "FILE_UPLOAD_ERROR", + TOOL_EXECUTION_ERROR: "TOOL_EXECUTION_ERROR", + AI_PROCESSING_ERROR: "AI_PROCESSING_ERROR", + FILE_DOWNLOAD_ERROR: "FILE_DOWNLOAD_ERROR", +}); + +const ERROR_NAME_TO_CODE_MAP = Object.freeze( + Object.keys(ERROR_NAMES).reduce((acc, key) => { + if (ERROR_CODES?.[key]) { + acc[ERROR_NAMES[key]] = ERROR_CODES[key]; + } + return acc; + }, {}) +); + +module.exports = { + GENDERS, + ERROR_NAMES, + ERROR_CODES, + ERROR_NAME_TO_CODE_MAP, +}; diff --git a/src/enums/user.enum.js b/src/enums/user.enum.js deleted file mode 100644 index 39fe1b1..0000000 --- a/src/enums/user.enum.js +++ /dev/null @@ -1,12 +0,0 @@ -const instituteUserStatus = Object.freeze({ - ACTIVE: "ACTIVE", - SUSPENDED: "SUSPENDED", - DEACTIVATED: "DEACTIVATED", -}); - -const gender = Object.freeze({ - MALE: "MALE", - FEMALE: "FEMALE", -}); - -module.exports = { gender, instituteUserStatus }; diff --git a/src/errors/error-classes.js b/src/errors/error-classes.js deleted file mode 100644 index ce29b07..0000000 --- a/src/errors/error-classes.js +++ /dev/null @@ -1,60 +0,0 @@ -class BaseError extends Error { - constructor(message, name, statusCode) { - super(message); - this.name = name; - this.statusCode = statusCode; - } -} - -class DatabaseConnectionError extends BaseError { - constructor(message = 'Failed to connect to the database') { - super(message, 'DatabaseConnectionError', 500); - } -} - -class ValidationError extends BaseError { - constructor(message = 'Validation failed') { - super(message, 'ValidationError', 400); - } -} - -class AuthenticationError extends BaseError { - constructor(message = 'Authentication failed') { - super(message, 'AuthenticationError', 401); - } -} - -class NotFoundError extends BaseError { - constructor(resource = 'Resource') { - super(`${resource} not found`, 'NotFoundError', 404); - } -} - -class DuplicateEntryError extends BaseError { - constructor(field = 'Entry') { - super(`${field} already exists`, 'DuplicateEntryError', 409); - } -} - -class UnauthorizedError extends BaseError { - constructor(message = 'Unauthorized access') { - super(message, 'UnauthorizedError', 403); - } -} - -class UserExistsError extends BaseError { - constructor(message = 'User with this email or username already exists') { - super(message, 'UserExistsError', 409); - } -} - -module.exports = { - BaseError, - DatabaseConnectionError, - ValidationError, - AuthenticationError, - NotFoundError, - DuplicateEntryError, - UnauthorizedError, - UserExistsError -}; \ No newline at end of file diff --git a/src/middleware/authenticate-account.middleware.js b/src/middleware/authenticate-account.middleware.js index c94debd..60f3bd6 100644 --- a/src/middleware/authenticate-account.middleware.js +++ b/src/middleware/authenticate-account.middleware.js @@ -1,4 +1,4 @@ -const { UnauthorizedError } = require("../errors/error-classes"); +const { UnauthorizedError } = require("../utils/errors/error-classes"); const { verifyToken } = require("../utils/jwt.util"); const authenticateUser = async (req, res, next) => { @@ -14,14 +14,8 @@ const authenticateUser = async (req, res, next) => { const decodedToken = verifyToken(token); - // const account = await instituteAccountService.( - // decodedToken.username - // ); - // if (!account) { - // throw new UnauthorizedError("Account not found"); - // } + req.user = decodedToken; - // req.account = account; next(); } catch (error) { next(error); diff --git a/src/middleware/error-handling.middleware.js b/src/middleware/error-handling.middleware.js index f098dae..8e4e5e3 100644 --- a/src/middleware/error-handling.middleware.js +++ b/src/middleware/error-handling.middleware.js @@ -1,12 +1,25 @@ -const { BaseError } = require("../errors/error-classes"); +const { BaseError } = require("../utils/errors/error-classes"); +const { response } = require("../utils/response.util"); +const { ERROR_CODES } = require("../enums"); + const errorHandlingMiddleware = (err, req, res, next) => { - console.log(err); + console.error(err); if (err instanceof BaseError) { - return res.status(err.statusCode).json({ message: err.message }); + return response.error({ + res, + message: err.message, + code: err.code, + statusCode: err.statusCode, + }); } - return res.status(500).json({ message: "Internal server error" }); + return response.error({ + res, + message: "An unexpected error occurred. Please try again later.", + code: ERROR_CODES.INTERNAL_ERROR, + statusCode: 500, + }); }; module.exports = errorHandlingMiddleware; diff --git a/src/models/index.js b/src/models/index.js index b3eace7..8adb317 100644 --- a/src/models/index.js +++ b/src/models/index.js @@ -1,10 +1,8 @@ const { Sequelize } = require("sequelize"); -const config = require("../config"); +const dbConfig = require("../configs").database; const fs = require("fs"); const path = require("path"); -const dbConfig = config.database; -const db = {}; const basename = path.basename(__filename); const sequelize = new Sequelize( @@ -14,28 +12,21 @@ const sequelize = new Sequelize( { host: dbConfig.host, port: dbConfig.port, - dialect: "postgres", - logging: console.log, + dialect: dbConfig.dialect, + logging: dbConfig.logging, define: { timestamps: true, }, - pool: { - max: 5, - min: 0, - acquire: 30000, - idle: 10000, + dialectOptions: { + ...dbConfig.dialectOptions, + statement_timeout: 60000, + connectionTimeoutMillis: 60000, }, + pool: dbConfig.pool, } ); -sequelize - .authenticate() - .then(() => { - console.log("Successfully connected to the database"); - }) - .catch((err) => { - console.log("Error connecting to the database", err); - }); +const db = {}; fs.readdirSync(__dirname) .filter( @@ -59,4 +50,16 @@ Object.keys(db).forEach((modelName) => { db.sequelize = sequelize; db.Sequelize = Sequelize; +sequelize + .authenticate() + .then(() => { + console.log("Successfully connected to the database"); + }) + .catch((err) => { + console.error( + "Warning: Could not establish database connection during initialization:", + err.message + ); + }); + module.exports = db; diff --git a/src/schemas/class.schema.js b/src/schemas/class.schema.js deleted file mode 100644 index 764dd21..0000000 --- a/src/schemas/class.schema.js +++ /dev/null @@ -1,9 +0,0 @@ -const Joi = require("joi"); - -const classSchema = Joi.object({ - class_name: Joi.string().max(255).required(), - section_name: Joi.string().max(255).required(), - teacher_name: Joi.string().max(255).allow(null).optional(), -}); - -module.exports = { classSchema }; diff --git a/src/schemas/institute.schema.js b/src/schemas/institute.schema.js deleted file mode 100644 index db0c4db..0000000 --- a/src/schemas/institute.schema.js +++ /dev/null @@ -1,59 +0,0 @@ -const Joi = require("joi"); -const { instituteType, fundingType } = require("../enums/institute.enum"); - -const updateInstituteSchema = Joi.object({ - // Basic Information - institute_code: Joi.string().max(255).optional(), - institute_name: Joi.string().max(255).optional(), - institute_type: Joi.string().valid(...Object.values(instituteType)).optional(), - - // Branding and Media - logo_url: Joi.string().uri().allow(null).optional(), - rect_logo_url: Joi.string().uri().allow(null).optional(), - banner_image_url: Joi.string().uri().allow(null).optional(), - color_primary: Joi.string() - .max(7) - .pattern(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/) - .optional(), - color_secondary: Joi.string() - .max(7) - .pattern(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/) - .optional(), - - // Contact Information - address: Joi.string().allow(null).optional(), - city: Joi.string().max(255).allow(null).optional(), - state: Joi.string().max(255).allow(null).optional(), - country: Joi.string().max(255).allow(null).optional(), - postal_code: Joi.string().max(20).allow(null).optional(), - phone_numbers: Joi.array().items(Joi.string().max(20)).allow(null).optional(), - email: Joi.string().email().max(255).allow(null).optional(), - website_url: Joi.string().uri().allow(null).optional(), - social_media_links: Joi.object().allow(null).optional(), - - // Operational Details - founded_year: Joi.number() - .integer() - .min(1800) - .max(new Date().getFullYear()) - .allow(null) - .optional(), - number_of_students: Joi.number().integer().min(0).allow(null).optional(), - faculty_count: Joi.number().integer().min(0).allow(null).optional(), - - // Additional Information - mission_statement: Joi.string().allow(null).optional(), - vision_statement: Joi.string().allow(null).optional(), - core_values: Joi.string().allow(null).optional(), - funding_type: Joi.string() - .valid(...Object.values(fundingType)) - .allow(null) - .optional(), - - // Administrative Details - status: Joi.string().max(50).valid("ACTIVE", "INACTIVE").optional(), -}).min(1); - -module.exports = { - updateInstituteSchema, -}; diff --git a/src/schemas/student.schema.js b/src/schemas/student.schema.js deleted file mode 100644 index b805bfa..0000000 --- a/src/schemas/student.schema.js +++ /dev/null @@ -1,155 +0,0 @@ -const Joi = require("joi"); - -const createStudentSchema = Joi.object({ - // Personal Information - first_name: Joi.string().max(50).required(), - last_name: Joi.string().max(50).required(), - avatar_url: Joi.string().max(255).uri().allow(null, "").optional(), - gender: Joi.string().valid("MALE", "FEMALE").required(), - date_of_birth: Joi.date().max("now").min("1900-01-01").required(), - religion: Joi.string().max(50).required(), - class_id: Joi.number().integer().required(), - nationality: Joi.string().max(50).allow(null, "").optional(), - blood_group: Joi.string().max(5).allow(null, "").optional(), - speaks_urdu: Joi.boolean().allow(null, "").optional(), - knowledge_of_english: Joi.boolean().allow(null, "").optional(), - handedness: Joi.string() - .valid("LEFT", "RIGHT", "AMBIDEXTROUS") - .allow(null, "") - .optional(), - physical_condition: Joi.boolean().required(), - disability_description: Joi.when("physical_condition", { - is: true, - then: Joi.string().max(255).required(), - otherwise: Joi.string().allow(null, ""), - }), - address: Joi.string().required(), - - // Parents/Guardian Details - father_name: Joi.string().max(100).required(), - mother_name: Joi.string().max(100).required(), - father_education: Joi.string().max(100).allow(null, "").optional(), - mother_education: Joi.string().max(100).allow(null, "").optional(), - father_occupation: Joi.string().max(100).allow(null, "").optional(), - mother_occupation: Joi.string().max(100).allow(null, "").optional(), - landline_number: Joi.string().max(15).allow(null, "").optional(), - cell_phone_number: Joi.string().max(15).required(), - - // Sibling Information - number_of_children: Joi.number().integer().allow(null, "").optional(), - child_position: Joi.number().integer().allow(null, "").optional(), - siblings_in_school: Joi.array() - .items(Joi.string().max(255)) - .allow(null, "") - .optional(), - - // Admission Details - admission_no: Joi.string().max(50).required(), - date_of_admission: Joi.date().min(Joi.ref("date_of_birth")).required(), - transport_id: Joi.when("school_transport_required", { - is: true, - then: Joi.number().integer().required(), - otherwise: Joi.number().integer().allow(null, ""), - }), - admitted_in_class: Joi.string().max(50).required(), - previous_school_name: Joi.string().required(), - school_leaving_certificate: Joi.boolean().required(), - principal_remarks: Joi.string().allow(null, "").optional(), - - // Fee Details - admission_fee: Joi.string().max(50).required(), - tuition_fee: Joi.string().max(50).required(), - school_transport_required: Joi.boolean().required(), - transport_fee: Joi.when("school_transport_required", { - is: true, - then: Joi.string().max(50).required(), - otherwise: Joi.string().max(50).allow(null, "").required(), - }), - security_fee: Joi.string().max(50).allow(null, "").optional(), - medical_fee: Joi.string().max(50).allow(null, "").optional(), - student_card: Joi.string().max(50).allow(null, "").optional(), -}); - -const studentfiltersSchema = Joi.object({ - search: Joi.string().allow("").optional(), - class_id: Joi.number().integer().optional(), - transport_id: Joi.number().integer().optional(), - paid_until: Joi.date().iso().optional(), - fees_status: Joi.string().valid("CURRENT", "WARNING", "DEFAULTER").optional(), -}); - -const updateStudentSchema = Joi.object({ - // Personal Information - first_name: Joi.string().max(50).optional(), - last_name: Joi.string().max(50).optional(), - avatar_url: Joi.string().max(255).uri().allow(null, "").optional(), - gender: Joi.string().valid("MALE", "FEMALE").optional(), - date_of_birth: Joi.date().max("now").min("1900-01-01").optional(), - religion: Joi.string().max(50).optional(), - class_id: Joi.number().integer().optional(), - nationality: Joi.string().max(50).allow(null, "").optional(), - blood_group: Joi.string().max(5).allow(null, "").optional(), - speaks_urdu: Joi.boolean().allow(null, "").optional(), - knowledge_of_english: Joi.boolean().allow(null, "").optional(), - handedness: Joi.string() - .valid("LEFT", "RIGHT", "AMBIDEXTROUS") - .allow(null, "") - .optional(), - physical_condition: Joi.boolean().optional(), - disability_description: Joi.when("physical_condition", { - is: true, - then: Joi.string().max(255).optional(), - otherwise: Joi.string().allow(null, "").optional(), - }), - address: Joi.string().optional(), - - // Parents/Guardian Details - father_name: Joi.string().max(100).optional(), - mother_name: Joi.string().max(100).optional(), - father_education: Joi.string().max(100).allow(null, "").optional(), - mother_education: Joi.string().max(100).allow(null, "").optional(), - father_occupation: Joi.string().max(100).allow(null, "").optional(), - mother_occupation: Joi.string().max(100).allow(null, "").optional(), - landline_number: Joi.string().max(15).allow(null, "").optional(), - cell_phone_number: Joi.string().max(15).optional(), - - // Sibling Information - number_of_children: Joi.number().integer().allow(null, "").optional(), - child_position: Joi.number().integer().allow(null, "").optional(), - siblings_in_school: Joi.array() - .items(Joi.string().max(255)) - .allow(null, "") - .optional(), - - // Admission Details - admission_no: Joi.string().max(50).optional(), - date_of_admission: Joi.date().min(Joi.ref("date_of_birth")).optional(), - transport_id: Joi.when("school_transport_required", { - is: true, - then: Joi.number().integer().optional(), - otherwise: Joi.number().integer().allow(null, "").optional(), - }), - admitted_in_class: Joi.string().max(50).optional(), - previous_school_name: Joi.string().optional(), - school_leaving_certificate: Joi.boolean().optional(), - principal_remarks: Joi.string().allow(null, "").optional(), - - // Fee Details - admission_fee: Joi.string().max(50).optional(), - tuition_fee: Joi.string().max(50).optional(), - school_transport_required: Joi.boolean().optional(), - transport_fee: Joi.when("school_transport_required", { - is: true, - then: Joi.string().max(50).optional(), - otherwise: Joi.string().max(50).allow(null, "").optional(), - }), - security_fee: Joi.string().max(50).allow(null, "").optional(), - medical_fee: Joi.string().max(50).allow(null, "").optional(), - student_card: Joi.string().max(50).allow(null, "").optional(), -}).min(1); - -module.exports = { - createStudentSchema, - studentfiltersSchema, - updateStudentSchema, -}; diff --git a/src/schemas/transport.schema.js b/src/schemas/transport.schema.js deleted file mode 100644 index 8cd23cd..0000000 --- a/src/schemas/transport.schema.js +++ /dev/null @@ -1,10 +0,0 @@ -const Joi = require("joi"); - -const createTransportSchema = Joi.object({ - transport_number: Joi.number().required(), - vehicle_number: Joi.string().max(50).required(), - route: Joi.string().max(100).required(), - driver_name: Joi.string().max(100).required(), -}); - -module.exports = { createTransportSchema }; diff --git a/src/utils/case.util.js b/src/utils/case.util.js new file mode 100644 index 0000000..cd0681a --- /dev/null +++ b/src/utils/case.util.js @@ -0,0 +1,52 @@ +const lodash = require("lodash"); + +/** + * Recursively converts all object keys to camelCase + * @param {object | array} data + * @returns {object | array} + */ +const toCamel = (data) => { + if (Array.isArray(data)) return data.map(toCamel); + if (lodash.isPlainObject(data)) { + return lodash.mapValues( + lodash.mapKeys(data, (_v, k) => lodash.camelCase(k)), + toCamel + ); + } + return data; +}; + +/** + * Recursively converts plain object keys to snake_case, + * preserving non-string keys (like Sequelize Op symbols). + * + * @param {any} data + * @returns {any} + */ +const toSnake = (data) => { + if (Array.isArray(data)) { + return data.map(toSnake); + } + + if (lodash.isPlainObject(data)) { + const stringKeyPairs = Object.keys(data).map((key) => { + const converted = lodash.snakeCase(key); + const newKey = converted === "" || converted === key ? key : converted; + return [newKey, toSnake(data[key])]; + }); + + const symbolKeyPairs = Object.getOwnPropertySymbols(data).map((symbol) => [ + symbol, + toSnake(data[symbol]), + ]); + + return Object.fromEntries([...stringKeyPairs, ...symbolKeyPairs]); + } + + return data; +}; + +module.exports = { + toCamel, + toSnake, +}; diff --git a/src/utils/errors/error-classes.js b/src/utils/errors/error-classes.js new file mode 100644 index 0000000..9190f1f --- /dev/null +++ b/src/utils/errors/error-classes.js @@ -0,0 +1,86 @@ +const { ERROR_CODES, ERROR_NAMES } = require("../../enums"); + +class BaseError extends Error { + constructor(message, name, code, statusCode) { + super(message); + this.name = name; + this.code = code; + this.statusCode = statusCode; + } +} + +class DatabaseConnectionError extends BaseError { + constructor(message = "Failed to connect to the database") { + super(message, ERROR_NAMES.DATABASE_CONNECTION_ERROR, ERROR_CODES.DATABASE_CONNECTION_ERROR, 500); + } +} + +class ValidationError extends BaseError { + constructor(message = "Validation failed") { + super(message, ERROR_NAMES.VALIDATION_ERROR, ERROR_CODES.VALIDATION_ERROR, 400); + } +} + +class AuthenticationError extends BaseError { + constructor(message = "Authentication failed") { + super(message, ERROR_NAMES.AUTHENTICATION_ERROR, ERROR_CODES.AUTHENTICATION_ERROR, 401); + } +} + +class NotFoundError extends BaseError { + constructor(resource = "Resource") { + super(`${resource} not found`, ERROR_NAMES.NOT_FOUND_ERROR, ERROR_CODES.NOT_FOUND_ERROR, 404); + } +} + +class DuplicateEntryError extends BaseError { + constructor(field = "Entry") { + super(`${field} already exists`, ERROR_NAMES.DUPLICATE_ENTRY_ERROR, ERROR_CODES.DUPLICATE_ENTRY_ERROR, 409); + } +} + +class UnauthorizedError extends BaseError { + constructor(message = "Unauthorized access") { + super(message, ERROR_NAMES.UNAUTHORIZED_ERROR, ERROR_CODES.UNAUTHORIZED_ERROR, 403); + } +} + +class UserExistsError extends BaseError { + constructor(message = "User with this email or username already exists") { + super(message, ERROR_NAMES.USER_EXISTS_ERROR, ERROR_CODES.USER_EXISTS_ERROR, 409); + } +} + +class UniqueConstraintError extends BaseError { + constructor(message = "Unique constraints failed") { + super(message, ERROR_NAMES.UNIQUE_CONSTRAINT_ERROR, ERROR_CODES.UNIQUE_CONSTRAINTS_ERROR, 409); + } +} + +class FileUploadError extends BaseError { + constructor(message = "File upload failed") { + super(message, ERROR_NAMES.FILE_UPLOAD_ERROR, ERROR_CODES.FILE_UPLOAD_ERROR, 400); + } +} + +class FileDownloadError extends BaseError { + constructor(message = "File download failed") { + super(message, ERROR_NAMES.FILE_DOWNLOAD_ERROR, ERROR_CODES.FILE_DOWNLOAD_ERROR, 400); + } +} + + + +module.exports = { + BaseError, + DatabaseConnectionError, + ValidationError, + AuthenticationError, + NotFoundError, + DuplicateEntryError, + UnauthorizedError, + UserExistsError, + UniqueConstraintError, + FileUploadError, + FileDownloadError, +}; diff --git a/src/utils/hash-password.util.js b/src/utils/hash-password.util.js new file mode 100644 index 0000000..bacc366 --- /dev/null +++ b/src/utils/hash-password.util.js @@ -0,0 +1,14 @@ +const bcrypt = require("bcrypt"); + +const hashPassword = async (password) => { + return await bcrypt.hash(password, 10); +}; + +const comparePassword = async (password, hash) => { + return await bcrypt.compare(password, hash); +}; + +module.exports = { + hashPassword, + comparePassword, +}; diff --git a/src/utils/isValidKsuid.util.js b/src/utils/isValidKsuid.util.js new file mode 100644 index 0000000..f0321dc --- /dev/null +++ b/src/utils/isValidKsuid.util.js @@ -0,0 +1,15 @@ +const KSUID = require("ksuid"); + +function isValidKsuid(id) { + if (typeof id !== 'string') return false; + if (!/^[0-9A-Za-z]{27}$/.test(id)) return false; + + try { + KSUID.parse(id); + return true; + } catch { + return false; + } +} + +module.exports = { isValidKsuid }; \ No newline at end of file diff --git a/src/utils/json.util.js b/src/utils/json.util.js new file mode 100644 index 0000000..6564489 --- /dev/null +++ b/src/utils/json.util.js @@ -0,0 +1,47 @@ +const { jsonrepair } = require("jsonrepair"); + +/** + * Clean a JSON string by removing control characters + * @param {string} text - The text to clean + * @returns {string} The cleaned text + */ +function cleanJsonString(text) { + if (typeof text !== "string") return text; + + return text.replace(/[\x00-\x1F\x7F-\x9F]/g, "") + .trim() + .replace(/^```json/i, "") + .replace(/^```/, "") + .replace(/```$/, "") + .trim(); +} + +/** + * Parse a JSON string safely + * @param {string} jsonString - The JSON string to parse + * @returns {object|null} The parsed JSON object or null if invalid + */ +function parseJsonSafely(jsonString) { + try { + if (typeof jsonString !== "string") { + throw new Error("Input is not a string"); + } + + let cleaned = cleanJsonString(jsonString) + + const match = cleaned.match(/\{[\s\S]*\}/); + if (match) cleaned = match[0]; + + const repaired = jsonrepair(cleaned); + + return JSON.parse(repaired); + } catch (error) { + console.error("JSON parsing failed:", error.message); + return null; + } +} + +module.exports = { + cleanJsonString, + parseJsonSafely, +}; diff --git a/src/utils/jwt.util.js b/src/utils/jwt.util.js index fc6343a..61e9d45 100644 --- a/src/utils/jwt.util.js +++ b/src/utils/jwt.util.js @@ -1,26 +1,16 @@ const jwt = require("jsonwebtoken"); -const { jwtSecret, jwtExpiresIn } = require("../config/jwt.config"); -const { UnauthorizedError } = require("../errors/error-classes"); - -const makePayload = (user) => { - return { - user_id: user.user_id, - username: user.username, - email_address: user.email_address, - created_at: user.created_at, - updated_at: user.updated_at, - deleted_at: user.deleted_at, - }; -}; +const { + jwt: { secret }, +} = require("../configs"); +const { UnauthorizedError } = require("../utils/errors/error-classes"); const generateToken = (user) => { - const payload = makePayload(user); - return jwt.sign(payload, jwtSecret, { expiresIn: jwtExpiresIn }); + return jwt.sign(user, secret, { expiresIn: "7d" }); }; const verifyToken = (token) => { try { - return jwt.verify(token, jwtSecret); + return jwt.verify(token, secret); } catch { throw new UnauthorizedError("Invalid or expired token!"); } diff --git a/src/utils/response.util.js b/src/utils/response.util.js new file mode 100644 index 0000000..181dd95 --- /dev/null +++ b/src/utils/response.util.js @@ -0,0 +1,73 @@ +/** + * Set response headers. + * + * @param {import('express').Response} res Express response object. + * @param {Record} headers Additional headers to set. + */ +const setResponseHeaders = (res, headers) => { + for (const [key, value] of Object.entries(headers)) { + res.setHeader(key, value); + } +}; + +/** + * Send a standardized success response. + * + * @param {object} options Options object. + * @param {import('express').Response} options.res Express response object. + * @param {object} [options.data={}] Response payload data. + * @param {string} [options.message="Success"] Human‑readable success message. + * @param {number} [options.statusCode=200] HTTP status code. + * @param {Record} [options.headers={}] Additional headers to set. + * @returns {import('express').Response} Express response object. + */ +const success = ({ + res, + data = {}, + message = "Success", + statusCode = 200, + headers = {}, +}) => { + setResponseHeaders(res, headers); + return res.status(statusCode).json({ + status: "success", + message, + data, + }); +}; + +/** + * Send a standardized error response. + * + * @param {object} options Options object. + * @param {import('express').Response} options.res Express response object. + * @param {string} options.message Human‑readable error message. + * @param {string} [options.code="InternalError"] Application‑level error code. + * @param {number} [options.statusCode=500] HTTP status code. + * @param {Record} [options.headers={}] Additional headers to set. + * @returns {import('express').Response} Express response object. + */ +const error = ({ + res, + message, + code = "InternalError", + statusCode = 500, + headers = {}, +}) => { + setResponseHeaders(res, headers); + return res.status(statusCode).json({ + status: "error", + code, + message, + }); +}; + +const response = {}; + +response.success = success; +response.error = error; + +module.exports = { + response, + setResponseHeaders, +}; diff --git a/src/utils/sanitizeData.util.js b/src/utils/sanitizeData.util.js new file mode 100644 index 0000000..6bf2498 --- /dev/null +++ b/src/utils/sanitizeData.util.js @@ -0,0 +1,14 @@ +/** + * Sanitize data by removing null and undefined values + * @param {object} data - Data to sanitize + * @returns {object} Sanitized data + */ +const sanitizeData = (data) => { + return Object.fromEntries( + Object.entries(data).filter( + ([_, value]) => value !== null && value !== undefined + ) + ); +}; + +module.exports = { sanitizeData }; diff --git a/src/utils/sequelize-filter.util.js b/src/utils/sequelize-filter.util.js new file mode 100644 index 0000000..ca477ef --- /dev/null +++ b/src/utils/sequelize-filter.util.js @@ -0,0 +1,119 @@ +const { Op } = require("sequelize"); + +/** + * Convert filter object to a Sequelize-compatible where clause. + * + * The filter object supports the following operators for each field: + * - in: `{ field: { in: [value1, value2, ...] } }` -–> **Op.in** + * - notIn: `{ field: { notIn: [value1, value2, ...] } }` --> **Op.notIn** + * - like: `{ field: { like: '%pattern%' } }` --> **Op.like** + * - notLike: `{ field: { notLike: '%pattern%' } }` --> **Op.notLike** + * - gt: `{ field: { gt: value } }` --> **Op.gt** + * - gte: `{ field: { gte: value } }` --> **Op.gte** + * - lt: `{ field: { lt: value } }` --> **Op.lt** + * - lte: `{ field: { lte: value } }` --> **Op.lte** + * - between: `{ field: { between: [min, max] } }` --> **Op.between** + * - notBetween:`{ field: { notBetween: [min, max] } }` --> **Op.notBetween** + * - is: `{ field: { is: value } }` --> **Op.is** (can be null) + * - not: `{ field: { not: value } }` --> **Op.not** + * - eq: `{ field: { eq: value } }` --> **Op.eq** + * - ne: `{ field: { ne: value } }` --> **Op.ne** + * + * Example: + * ```javascript + * { + * name: { like: '%John%' }, + * age: { gte: 18, lte: 65 }, + * status: { in: ['active', 'pending'] } + * } + * ``` + * + * @param {object} filters - Filter object with supported operators (see above) + * @returns {object|null} Sequelize-compatible where clause, or null if no filters + */ +const buildSequelizeWhereClause = (filters) => { + if ( + !filters || + typeof filters !== "object" || + Object.keys(filters).length === 0 + ) { + return null; + } + + const converted = {}; + + for (const [key, value] of Object.entries(filters)) { + if (value && typeof value === "object" && !Array.isArray(value)) { + const sequelizeOperators = {}; + + for (const [operator, operatorValue] of Object.entries(value)) { + switch (operator) { + case "in": + sequelizeOperators[Op.in] = operatorValue; + break; + case "notIn": + sequelizeOperators[Op.notIn] = operatorValue; + break; + case "like": + sequelizeOperators[Op.like] = operatorValue; + break; + case "notLike": + sequelizeOperators[Op.notLike] = operatorValue; + break; + case "gt": + sequelizeOperators[Op.gt] = operatorValue; + break; + case "gte": + sequelizeOperators[Op.gte] = operatorValue; + break; + case "lt": + sequelizeOperators[Op.lt] = operatorValue; + break; + case "lte": + sequelizeOperators[Op.lte] = operatorValue; + break; + case "between": + sequelizeOperators[Op.between] = operatorValue; + break; + case "notBetween": + sequelizeOperators[Op.notBetween] = operatorValue; + break; + case "is": + sequelizeOperators[Op.is] = operatorValue; + break; + case "not": + sequelizeOperators[Op.not] = operatorValue; + break; + case "and": + sequelizeOperators[Op.and] = operatorValue; + break; + case "or": + sequelizeOperators[Op.or] = operatorValue; + break; + default: + console.warn( + `Invalid sequelize operator [${operator}], falling back to direct value` + ); + sequelizeOperators[operator] = operatorValue; + break; + } + } + + converted[key] = sequelizeOperators; + } else { + // Direct value assignment + converted[key] = value; + } + } + + // Return null if no valid filters were converted + if (Object.keys(converted).length === 0) { + return null; + } + + return converted; +}; + +module.exports = { + buildSequelizeWhereClause, +}; diff --git a/src/utils/validation.util.js b/src/utils/validation.util.js new file mode 100644 index 0000000..bc4c8c5 --- /dev/null +++ b/src/utils/validation.util.js @@ -0,0 +1,21 @@ +const { ValidationError } = require("../utils/errors/error-classes"); + +/** + * Validate data using a validator function + * @param {Function} validator - Validator function + * @param {object} data - Data to validate + * @returns {object} Validated data + */ +const validateData = (validator, data) => { + const { error, value } = validator(data); + + if (error) { + throw new ValidationError(error.details.map((d) => d.message).join(", ")); + } + + return value; +}; + +module.exports = { + validateData, +}; From 67dd078b6151469bc4a1cf0788eae08d903697f2 Mon Sep 17 00:00:00 2001 From: Ahmad Shah Date: Mon, 13 Oct 2025 16:11:13 +0500 Subject: [PATCH 2/4] Enhance example controller with improved response handling, add example user creation functionality, and example validator for login schema --- src/controllers/auth.controller.js | 10 ++-- src/database/migrations/.gitkeep | 0 .../20241204153439-create-users-table.js | 51 ------------------- src/middleware/error-handling.middleware.js | 6 +-- src/schemas/auth.schema.js | 5 ++ src/services/user.service.js | 13 +++-- src/utils/hash-password.util.js | 11 ++++ src/utils/isValidKsuid.util.js | 5 ++ src/utils/jwt.util.js | 11 ++++ src/utils/password.util.js | 11 ---- src/utils/response.util.js | 41 ++++++++++++--- 11 files changed, 86 insertions(+), 78 deletions(-) create mode 100644 src/database/migrations/.gitkeep delete mode 100644 src/database/migrations/20241204153439-create-users-table.js delete mode 100644 src/utils/password.util.js diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js index 3c88ca6..ed830b2 100644 --- a/src/controllers/auth.controller.js +++ b/src/controllers/auth.controller.js @@ -3,6 +3,7 @@ const { loginSchema } = require("../schemas/auth.schema"); const userService = require("../services/user.service"); const { generateToken } = require("../utils/jwt.util"); const { comparePassword } = require("../utils/password.util"); +const { response } = require("../utils/response.util"); const login = async (req, res, next) => { try { @@ -28,9 +29,12 @@ const login = async (req, res, next) => { user.password = undefined; const userDetails = user.toJSON(); - res.status(200).json({ - user: {userDetails}, - token: generateToken(user), + response(res).success({ + message: "Login successful", + data: { + user: userDetails, + token: generateToken({ id: userDetails.id, username: userDetails.username }), + }, }); } catch (error) { next(error); diff --git a/src/database/migrations/.gitkeep b/src/database/migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/database/migrations/20241204153439-create-users-table.js b/src/database/migrations/20241204153439-create-users-table.js deleted file mode 100644 index 5238016..0000000 --- a/src/database/migrations/20241204153439-create-users-table.js +++ /dev/null @@ -1,51 +0,0 @@ -"use strict"; - -const tableName = "users"; - -/** @type {import('sequelize-cli').Migration} */ -module.exports = { - async up(queryInterface, Sequelize) { - await queryInterface.createTable(tableName, { - user_id: { - type: Sequelize.INTEGER, - autoIncrement: true, - primaryKey: true, - allowNull: false, - }, - username: { - type: Sequelize.STRING(255), - allowNull: false, - unique: true, - }, - password: { - type: Sequelize.TEXT, - allowNull: false, - }, - email_address: { - type: Sequelize.STRING(255), - allowNull: false, - unique: true, - }, - date_joined: { - type: Sequelize.DATE, - defaultValue: Sequelize.NOW, - }, - is_verified: { - type: Sequelize.BOOLEAN, - defaultValue: false, - }, - created_at: { - type: Sequelize.DATE, - defaultValue: Sequelize.NOW, - }, - updated_at: { - type: Sequelize.DATE, - defaultValue: Sequelize.NOW, - }, - }); - }, - - async down(queryInterface, Sequelize) { - await queryInterface.dropTable(tableName); - }, -}; diff --git a/src/middleware/error-handling.middleware.js b/src/middleware/error-handling.middleware.js index 8e4e5e3..43b3604 100644 --- a/src/middleware/error-handling.middleware.js +++ b/src/middleware/error-handling.middleware.js @@ -6,16 +6,14 @@ const errorHandlingMiddleware = (err, req, res, next) => { console.error(err); if (err instanceof BaseError) { - return response.error({ - res, + return response(res).error({ message: err.message, code: err.code, statusCode: err.statusCode, }); } - return response.error({ - res, + return response(res).error({ message: "An unexpected error occurred. Please try again later.", code: ERROR_CODES.INTERNAL_ERROR, statusCode: 500, diff --git a/src/schemas/auth.schema.js b/src/schemas/auth.schema.js index 0290091..d5af097 100644 --- a/src/schemas/auth.schema.js +++ b/src/schemas/auth.schema.js @@ -5,6 +5,11 @@ const loginSchema = Joi.object({ password: Joi.string().required(), }); +const validateLogin = (data) => loginSchema.validate(data); + + module.exports = { loginSchema, + + validateLogin, }; diff --git a/src/services/user.service.js b/src/services/user.service.js index 338a8cf..bd4c3d8 100644 --- a/src/services/user.service.js +++ b/src/services/user.service.js @@ -1,8 +1,15 @@ -const { User} = require("../models"); +const { User } = require("../models"); +const { toCamel, toSnake } = require("../utils/case.util"); const getUserByUsername = async (username) => { - const user = await User.findOne({where: { username }}); + const user = await User.findOne({ where: { username } }); return user; }; -module.exports = {getUserByUsername}; +// Example of how to use the toCamel and toSnake utils +const createUser = async (userData) => { + const user = await User.create(toSnake(userData)); + return user ? toCamel(user.toJSON()) : null; +} + +module.exports = { getUserByUsername, createUser }; diff --git a/src/utils/hash-password.util.js b/src/utils/hash-password.util.js index bacc366..86645c8 100644 --- a/src/utils/hash-password.util.js +++ b/src/utils/hash-password.util.js @@ -1,9 +1,20 @@ const bcrypt = require("bcrypt"); +/** + * Hashes a plain text password. + * @param {string} password - The plain text password to hash. + * @returns {Promise} - The hashed password. + */ const hashPassword = async (password) => { return await bcrypt.hash(password, 10); }; +/** + * Compares a plain text password with a hashed password. + * @param {string} password - The plain text password to compare. + * @param {string} hash - The hashed password to compare against. + * @returns {Promise} - True if the passwords match, false otherwise. + */ const comparePassword = async (password, hash) => { return await bcrypt.compare(password, hash); }; diff --git a/src/utils/isValidKsuid.util.js b/src/utils/isValidKsuid.util.js index f0321dc..15e2459 100644 --- a/src/utils/isValidKsuid.util.js +++ b/src/utils/isValidKsuid.util.js @@ -1,5 +1,10 @@ const KSUID = require("ksuid"); +/** + * Checks if a string is a valid KSUID. + * @param {string} id - The string to check. + * @returns {boolean} - True if the string is a valid KSUID, false otherwise. + */ function isValidKsuid(id) { if (typeof id !== 'string') return false; if (!/^[0-9A-Za-z]{27}$/.test(id)) return false; diff --git a/src/utils/jwt.util.js b/src/utils/jwt.util.js index 61e9d45..62b56d7 100644 --- a/src/utils/jwt.util.js +++ b/src/utils/jwt.util.js @@ -4,10 +4,21 @@ const { } = require("../configs"); const { UnauthorizedError } = require("../utils/errors/error-classes"); +/** + * Generates a JWT token for a user. + * @param {Object} user - The user object to encode in the token. + * @returns {string} - The generated JWT token. + */ const generateToken = (user) => { return jwt.sign(user, secret, { expiresIn: "7d" }); }; +/** + * Verifies a JWT token and returns the decoded payload. + * @param {string} token - The JWT token to verify. + * @returns {Object} - The decoded token payload. + * @throws {UnauthorizedError} - If the token is invalid or expired. + */ const verifyToken = (token) => { try { return jwt.verify(token, secret); diff --git a/src/utils/password.util.js b/src/utils/password.util.js deleted file mode 100644 index 46a758f..0000000 --- a/src/utils/password.util.js +++ /dev/null @@ -1,11 +0,0 @@ -const bcrypt = require("bcrypt"); - -const hashPassword = async (password) => { - return await bcrypt.hash(password, 10); -}; - -const comparePassword = async (password, hashedPassword) => { - return await bcrypt.compare(password, hashedPassword); -}; - -module.exports = { hashPassword, comparePassword }; diff --git a/src/utils/response.util.js b/src/utils/response.util.js index 181dd95..84cd02c 100644 --- a/src/utils/response.util.js +++ b/src/utils/response.util.js @@ -4,7 +4,9 @@ * @param {import('express').Response} res Express response object. * @param {Record} headers Additional headers to set. */ -const setResponseHeaders = (res, headers) => { +const setResponseHeaders = (res, headers = {}) => { + if (!headers || typeof headers !== "object") return; + for (const [key, value] of Object.entries(headers)) { res.setHeader(key, value); } @@ -31,8 +33,10 @@ const success = ({ setResponseHeaders(res, headers); return res.status(statusCode).json({ status: "success", + statusCode, message, data, + timestamp: new Date().toISOString(), }); }; @@ -57,17 +61,42 @@ const error = ({ setResponseHeaders(res, headers); return res.status(statusCode).json({ status: "error", + statusCode, code, message, + timestamp: new Date().toISOString(), }); }; -const response = {}; +/** + * @typedef {object} ResponseHandler + * @property {(options: {data?: object, message?: string, statusCode?: number, headers?: Record}) => import('express').Response} success + * @property {(options: {message: string, code?: string, statusCode?: number, headers?: Record}) => import('express').Response} error + */ + +/** + * Creates factory functions for success and error responses. + * + * @param {import('express').Response} res Express response object. + * @returns {ResponseHandler} Object with success and error methods. + */ +const response = (res) => { + const successHandler = ({ data, message, statusCode, headers }) => + success({ res, data, message, statusCode, headers }); + + const errorHandler = ({ message, code, statusCode, headers }) => + error({ res, message, code, statusCode, headers }); + + return { + success: successHandler, + error: errorHandler, + }; +}; -response.success = success; -response.error = error; -module.exports = { +module.exports = Object.freeze({ response, + success, + error, setResponseHeaders, -}; +}); From f08f2ff0fccc1eaa707198bf8f6fb032de664e82 Mon Sep 17 00:00:00 2001 From: Ahmad Shah Date: Mon, 13 Oct 2025 16:23:52 +0500 Subject: [PATCH 3/4] Refactor server startup logic, enhance error handling, and add health check endpoint --- app.js | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 6 deletions(-) diff --git a/app.js b/app.js index 5f87eb7..6128c43 100644 --- a/app.js +++ b/app.js @@ -1,26 +1,81 @@ +require("dotenv").config(); + const express = require("express"); -const config = require("./src/config"); +const config = require("./src/configs"); const errorHandlingMiddleware = require("./src/middleware/error-handling.middleware"); +const { response } = require("./src/utils/response.util"); const cors = require("cors"); const router = require("./src/routes"); const app = express(); app.use(express.json()); + +app.use(express.urlencoded({ extended: true })); + app.use( cors({ origin: "*", - methods: "*", - allowedHeaders: "*", + methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], + allowedHeaders: ["Content-Type", "Authorization"], }) ); app.use("/api", router); +app.get("/health", (req, res) => { + return response(res).success({ + data: { + status: "healthy", + timestamp: new Date().toISOString(), + environment: process.env.NODE_ENV || "development", + }, + }); +}); + app.use(errorHandlingMiddleware); const PORT = config.server.port || 5000; -app.listen(PORT, () => { - console.log(`Server is running on port ${PORT}`); -}); +// Start server +const startServer = () => { + try { + const server = app.listen(PORT, "0.0.0.0", () => { + console.log(`Server is running on port ${PORT}`); + }); + + // Handle server errors + server.on("error", (error) => { + console.error("Server error:", error); + process.exit(1); + }); + + // Handle unhandled promise rejections + process.on("unhandledRejection", (reason, promise) => { + console.error("Unhandled Rejection at:", promise, "reason:", reason); + }); + + // Handle uncaught exceptions + process.on("uncaughtException", (error) => { + console.error("Uncaught Exception:", error); + process.exit(1); + }); + + const shutdown = () => { + console.log("Shutdown signal received. Closing server..."); + server.close(() => { + console.log("Server closed. Exiting."); + process.exit(0); + }); + }; + + // Graceful shutdown + process.on("SIGTERM", shutdown); + process.on("SIGINT", shutdown); + } catch (error) { + console.error("Failed to start server:", error); + process.exit(1); + } +}; + +startServer(); From ff00ae920e4a70a7afdfd041a1a27489d5a562ce Mon Sep 17 00:00:00 2001 From: Ahmad Shah Date: Mon, 13 Oct 2025 18:09:26 +0500 Subject: [PATCH 4/4] feat: update package.json for version 2.0.0 and add new migration and seed scripts refactor: change error class import paths in auth.controller.js fix: update database config import path in models/index.js refactor: modify user model to use KSUID for user_id and update password utility import --- README.md | 230 +++++++- app.js | 2 +- package-lock.json | 879 ++++++++++++++++++++++++++++- package.json | 25 +- src/controllers/auth.controller.js | 4 +- src/models/index.js | 2 +- src/models/user.model.js | 11 +- 7 files changed, 1112 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 48e06f5..e1ea279 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,212 @@ -# Backend Boilerplate Express Backend +# Backend Boilerplate Express -Express Backend Application for company wide use in Algoirthm.io. -Tech Stack: +A production-ready Express backend boilerplate for company-wide use at Algorithm.io. -- Node.js `(18.x or above)` -- Express `(5.x)` -- PostgreSQL `(16.x or above)` -- Sequelize `(6.x)` +## Tech Stack -## Setup +- **Node.js** `(18.x or above)` +- **Express** `(5.x)` +- **PostgreSQL** `(16.x or above)` +- **Sequelize ORM** `(6.x)` +- **JWT** for authentication +- **Bcrypt** for password hashing +- **Joi** for request validation +- **KSUID** for unique identifiers +- **Lodash** for utility functions +- **JSON Repair** for JSON parsing and validation -1. Install dependencies: `npm install` -2. Configure environment variables from .env.example -3. Run migrations: `npm run migrate` -4. Start server: `npm start` +## Features + +- ✅ JWT-based authentication +- ✅ Centralized error handling +- ✅ Request validation with Joi schemas +- ✅ Database migrations with Sequelize +- ✅ Structured logging and responses +- ✅ CORS enabled +- ✅ Health check endpoint +- ✅ Clean architecture with separation of concerns + +## Getting Started + +### Prerequisites + +- Node.js 18.x or above +- PostgreSQL 16.x or above +- npm or yarn + +### Installation + +1. **Install dependencies:** + ```bash + npm install + ``` + +2. **Configure environment variables:** + Create a `.env` file in the root directory (use `.env.example` as a template) + +3. **Run database migrations:** + ```bash + npm run migrate + ``` + +4. **Start the server:** + - Development: `npm run dev` + - Production: `npm start` ## Project Structure -- `/logs` - Application logs -- `/src` - - `/config` - Configuration files - - `/controllers` - Request handlers - - `/database` - Database-related files - - `/enums` - Enumeration definitions - - `/errors` - Custom error definitions - - `/middleware` - Express middleware - - `/models` - Database models - - `/routes` - API route definitions - - `/services` - Business logic - - `/utils` - Utility functions - - `/validations` - Request validation schemas +``` +backend-boilerplate-express/ +├── app.js # Application entry point +├── package.json # Project dependencies and scripts +├── src/ +│ ├── config/ # Configuration files (database, JWT, etc.) +│ │ ├── database.config.js +│ │ ├── jwt.config.js +│ │ └── index.js +│ ├── controllers/ # Request handlers and business logic orchestration +│ │ └── auth.controller.js +│ ├── database/ # Database-related files +│ │ ├── migrations/ # Sequelize migrations +│ │ └── seeders/ # Database seeders +│ ├── enums/ # Application-wide enumerations and constants +│ │ └── index.js +│ ├── middleware/ # Express middleware functions +│ │ ├── authenticate-account.middleware.js +│ │ └── error-handling.middleware.js +│ ├── models/ # Sequelize models +│ │ ├── index.js +│ │ └── user.model.js +│ ├── routes/ # API route definitions +│ │ ├── index.js +│ │ └── auth.route.js +│ ├── schemas/ # Joi validation schemas +│ │ └── auth.schema.js +│ ├── services/ # Business logic layer +│ │ └── user.service.js +│ └── utils/ # Utility functions and helpers +│ ├── case.util.js +│ ├── hash-password.util.js +│ ├── isValidKsuid.util.js +│ ├── json.util.js +│ ├── jwt.util.js +│ ├── pagination-metadata.util.js +│ ├── response.util.js +│ ├── sanitizeData.util.js +│ ├── sequelize-filter.util.js +│ ├── validation.util.js +│ └── errors/ +│ └── error-classes.js +``` + +## Architecture + +This boilerplate follows a **layered architecture** pattern: + +1. **Routes Layer** (`/routes`) - Defines API endpoints and maps them to controllers +2. **Controller Layer** (`/controllers`) - Handles HTTP requests/responses and orchestrates services +3. **Service Layer** (`/services`) - Contains business logic and data access +4. **Model Layer** (`/models`) - Defines database schemas and relationships +5. **Middleware Layer** (`/middleware`) - Authentication, validation, error handling +6. **Utils Layer** (`/utils`) - Reusable helper functions + +## Available Scripts + +### Server +- `npm start` - Start the production server +- `npm run dev` - Start development server with hot reload (nodemon) + +### Database Migrations +- `npm run migrate` - Run all pending database migrations +- `npm run migrate:create -- ` - Create a new migration file +- `npm run migrate:undo -- ` - Undo a specific migration by name +- `npm run migrate:undo:last` - Undo the last migration +- `npm run migrate:undo:all` - Undo all migrations + +### Database Seeders +- `npm run seed` - Run all seeders +- `npm run seed:create -- ` - Create a new seeder file +- `npm run seed:run -- ` - Run a specific seeder +- `npm run seed:undo -- ` - Undo a specific seeder +- `npm run seed:undo:last` - Undo the last seeder +- `npm run seed:undo:all` - Undo all seeders + +### Utilities +- `npm run setup` - Run all migrations and seeders (useful for initial setup) +- `npm run reset` - Reset database completely (undo all migrations, run migrations, run seeders) + +## API Endpoints + +### Health Check +- `GET /health` - Returns server health status + +### Authentication +- `POST /api/auth/login` - User login + +## Environment Variables + +Create a `.env` file with the following variables: + +```env +# Server +PORT=5000 +NODE_ENV=development + +# Database +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=your_database +DB_USER=your_username +DB_PASSWORD=your_password + +# JWT +JWT_SECRET=your_jwt_secret +JWT_EXPIRES_IN=24h +JWT_REFRESH_SECRET=your_refresh_secret +JWT_REFRESH_EXPIRES_IN=7d +``` + +## Error Handling + +The application uses centralized error handling with custom error classes: + +- `ValidationError` - For validation failures +- `NotFoundError` - For resource not found +- `UnauthorizedError` - For authentication failures +- `ForbiddenError` - For authorization failures +- `ConflictError` - For data conflicts +- `InternalServerError` - For server errors + +## Response Format + +All API responses follow a consistent format: + +**Success Response:** +```json +{ + "success": true, + "data": { ... }, + "metadata": { ... } +} +``` + +**Error Response:** +```json +{ + "success": false, + "error": { + "code": "ERROR_CODE", + "message": "Error message" + } +} +``` + +## Contributing + +1. Create a feature branch from `main` +2. Make your changes +3. Submit a pull request + +## License + +ISC diff --git a/app.js b/app.js index 6128c43..b20c96f 100644 --- a/app.js +++ b/app.js @@ -1,7 +1,7 @@ require("dotenv").config(); const express = require("express"); -const config = require("./src/configs"); +const config = require("./src/config"); const errorHandlingMiddleware = require("./src/middleware/error-handling.middleware"); const { response } = require("./src/utils/response.util"); const cors = require("cors"); diff --git a/package-lock.json b/package-lock.json index 9a52fb9..faef48d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "backend-boilerplate-express", - "version": "1.0.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "backend-boilerplate-express", - "version": "1.0.0", + "version": "2.0.0", "license": "ISC", "dependencies": { "bcrypt": "^5.1.1", @@ -14,11 +14,17 @@ "dotenv": "^16.4.7", "express": "^5.0.1", "joi": "^17.13.3", + "jsonrepair": "^3.8.0", "jsonwebtoken": "^9.0.2", - "nodemon": "^3.1.9", + "ksuid": "^3.0.0", + "lodash": "^4.17.21", "pg": "^8.13.1", "pg-hstore": "^2.3.4", "sequelize": "^6.37.5" + }, + "devDependencies": { + "nodemon": "^3.1.9", + "sequelize-cli": "^6.6.2" } }, "node_modules/@hapi/hoek": { @@ -36,6 +42,109 @@ "@hapi/hoek": "^9.0.0" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", @@ -56,6 +165,24 @@ "node-pre-gyp": "bin/node-pre-gyp" } }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", @@ -170,10 +297,27 @@ "node": ">=8" } }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -209,12 +353,28 @@ "integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==", "license": "MIT" }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base-convert-int-array": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/base-convert-int-array/-/base-convert-int-array-1.0.1.tgz", + "integrity": "sha512-NWqzaoXx8L/SS32R+WmKqnQkVXVYl2PwNJ68QV3RAlRRL1uV+yxJT66abXI1cAvqCXQTyXr7/9NN4Af90/zDVw==", + "license": "ISC" + }, "node_modules/bcrypt": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", @@ -233,6 +393,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -241,6 +402,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.0.2.tgz", @@ -334,6 +502,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -390,6 +559,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -419,6 +589,38 @@ "node": ">=10" } }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", @@ -428,12 +630,33 @@ "color-support": "bin.js" } }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -492,6 +715,21 @@ "node": ">= 0.10" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/debug": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", @@ -575,6 +813,13 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -584,6 +829,51 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/editorconfig": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", + "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -635,6 +925,16 @@ "node": ">= 0.4" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -697,6 +997,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -747,6 +1048,36 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -765,6 +1096,22 @@ "node": ">= 0.8" } }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -799,6 +1146,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -839,6 +1187,16 @@ "node": ">=10" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", @@ -901,6 +1259,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -921,10 +1280,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -1028,6 +1395,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, "license": "ISC" }, "node_modules/inflection": { @@ -1056,6 +1424,13 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -1069,6 +1444,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -1077,10 +1453,27 @@ "node": ">=8" } }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -1099,6 +1492,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -1111,6 +1505,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -1122,6 +1517,29 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/joi": { "version": "17.13.3", "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", @@ -1135,6 +1553,143 @@ "@sideway/pinpoint": "^2.0.0" } }, + "node_modules/js-beautify": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", + "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.4.2", + "js-cookie": "^3.0.5", + "nopt": "^7.2.1" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-beautify/node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/js-beautify/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/js-beautify/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-beautify/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-beautify/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/js-beautify/node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonrepair": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/jsonrepair/-/jsonrepair-3.13.1.tgz", + "integrity": "sha512-WJeiE0jGfxYmtLwBTEk8+y/mYcaleyLXWaqp5bJu0/ZTSeG0KQq/wWQ8pmnkKenEdN6pdnn6QtcoSUkbqDHWNw==", + "license": "ISC", + "bin": { + "jsonrepair": "bin/cli.js" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -1184,6 +1739,15 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/ksuid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ksuid/-/ksuid-3.0.0.tgz", + "integrity": "sha512-81CkBGn/06ZVAjGvFZi6fVG8VcPeMH0JpJ4V1Z9VwrMMaGIeAjY4jrVdrIcxhL9I2ZUU6t5uiyswcmkk+KZegA==", + "license": "MIT", + "dependencies": { + "base-convert-int-array": "^1.0.1" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -1232,6 +1796,13 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -1440,6 +2011,7 @@ "version": "3.1.9", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==", + "dev": true, "license": "MIT", "dependencies": { "chokidar": "^3.5.2", @@ -1468,6 +2040,7 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1485,6 +2058,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/nopt": { @@ -1506,6 +2080,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -1566,6 +2141,13 @@ "wrappy": "1" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1584,6 +2166,40 @@ "node": ">=0.10.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-to-regexp": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", @@ -1694,10 +2310,18 @@ "split2": "^4.1.0" } }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -1745,6 +2369,13 @@ "node": ">=0.10.0" } }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1762,6 +2393,7 @@ "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, "license": "MIT" }, "node_modules/qs": { @@ -1833,6 +2465,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -1841,6 +2474,37 @@ "node": ">=8.10.0" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/retry-as-promised": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.0.4.tgz", @@ -2040,6 +2704,29 @@ } } }, + "node_modules/sequelize-cli": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/sequelize-cli/-/sequelize-cli-6.6.3.tgz", + "integrity": "sha512-1YYPrcSRt/bpMDDSKM5ubY1mnJ2TEwIaGZcqITw4hLtGtE64nIqaBnLtMvH8VKHg6FbWpXTiFNc2mS/BtQCXZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fs-extra": "^9.1.0", + "js-beautify": "1.15.4", + "lodash": "^4.17.21", + "picocolors": "^1.1.1", + "resolve": "^1.22.1", + "umzug": "^2.3.0", + "yargs": "^16.2.0" + }, + "bin": { + "sequelize": "lib/sequelize", + "sequelize-cli": "lib/sequelize" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/sequelize-pool": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", @@ -2099,6 +2786,29 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -2181,6 +2891,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, "license": "MIT", "dependencies": { "semver": "^7.5.3" @@ -2230,6 +2941,22 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -2242,10 +2969,25 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^3.0.0" @@ -2254,6 +2996,19 @@ "node": ">=4" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", @@ -2275,6 +3030,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -2302,6 +3058,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, "license": "ISC", "bin": { "nodetouch": "bin/nodetouch.js" @@ -2327,10 +3084,24 @@ "node": ">= 0.6" } }, + "node_modules/umzug": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz", + "integrity": "sha512-Z274K+e8goZK8QJxmbRPhl89HPO1K+ORFtm6rySPhFKfKc5GHhqdzD0SGhSWHkzoXasqJuItdhorSvY7/Cgflw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "^3.7.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, "license": "MIT" }, "node_modules/underscore": { @@ -2345,6 +3116,16 @@ "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "license": "MIT" }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -2412,6 +3193,22 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", @@ -2430,6 +3227,43 @@ "@types/node": "*" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2445,11 +3279,50 @@ "node": ">=0.4" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } } } } diff --git a/package.json b/package.json index 78d5098..732aeb1 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,24 @@ { "name": "backend-boilerplate-express", - "version": "1.0.0", - "description": "Express Backend Application for company wide use in Algoirthm.io.", - "main": "index.js", + "version": "2.0.0", + "description": "Express Backend Application for company wide use in Algorithm.io.", + "main": "app.js", "scripts": { "start": "node app.js", "dev": "nodemon app.js", "migrate": "npx sequelize-cli db:migrate", "migrate:create": "npx sequelize-cli migration:generate --name", "migrate:undo": "npx sequelize-cli db:migrate:undo --name", - "migrate:undo:all": "npx sequelize-cli db:migrate:undo:all" + "migrate:undo:last": "npx sequelize-cli db:migrate:undo", + "migrate:undo:all": "npx sequelize-cli db:migrate:undo:all", + "seed": "npx sequelize-cli db:seed:all", + "seed:create": "npx sequelize-cli seed:generate --name", + "seed:run": "npx sequelize-cli db:seed --seed", + "seed:undo": "npx sequelize-cli db:seed:undo --seed", + "seed:undo:last": "npx sequelize-cli db:seed:undo", + "seed:undo:all": "npx sequelize-cli db:seed:undo:all", + "setup": "npm run migrate && npm run seed", + "reset": "npm run migrate:undo:all && npm run migrate && npm run seed" }, "keywords": [], "author": "", @@ -20,10 +29,16 @@ "dotenv": "^16.4.7", "express": "^5.0.1", "joi": "^17.13.3", + "jsonrepair": "^3.8.0", "jsonwebtoken": "^9.0.2", - "nodemon": "^3.1.9", + "ksuid": "^3.0.0", + "lodash": "^4.17.21", "pg": "^8.13.1", "pg-hstore": "^2.3.4", "sequelize": "^6.37.5" + }, + "devDependencies": { + "nodemon": "^3.1.9", + "sequelize-cli": "^6.6.2" } } diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js index ed830b2..b9b6b98 100644 --- a/src/controllers/auth.controller.js +++ b/src/controllers/auth.controller.js @@ -1,8 +1,8 @@ -const { ValidationError, NotFoundError, UnauthorizedError } = require("../errors/error-classes"); +const { ValidationError, NotFoundError, UnauthorizedError } = require("../utils/errors/error-classes"); const { loginSchema } = require("../schemas/auth.schema"); const userService = require("../services/user.service"); const { generateToken } = require("../utils/jwt.util"); -const { comparePassword } = require("../utils/password.util"); +const { comparePassword } = require("../utils/hash-password.util"); const { response } = require("../utils/response.util"); const login = async (req, res, next) => { diff --git a/src/models/index.js b/src/models/index.js index 8adb317..72f8f60 100644 --- a/src/models/index.js +++ b/src/models/index.js @@ -1,5 +1,5 @@ const { Sequelize } = require("sequelize"); -const dbConfig = require("../configs").database; +const dbConfig = require("../config").database; const fs = require("fs"); const path = require("path"); diff --git a/src/models/user.model.js b/src/models/user.model.js index e2446ba..2d7b04e 100644 --- a/src/models/user.model.js +++ b/src/models/user.model.js @@ -1,12 +1,12 @@ -const { hashPassword } = require("../utils/password.util"); +const KSUID = require("ksuid"); +const { hashPassword } = require("../utils/hash-password.util"); module.exports = (sequelize, DataTypes) => { const User = sequelize.define( "User", { user_id: { - type: DataTypes.INTEGER, - autoIncrement: true, + type: DataTypes.STRING(27), primaryKey: true, allowNull: false, }, @@ -45,8 +45,11 @@ module.exports = (sequelize, DataTypes) => { tableName: "users", createdAt: "created_at", updatedAt: "updated_at", - paranoid: true, + paranoid: true, hooks: { + beforeValidate: async (user) => { + if (!user.user_id) user.user_id = (await KSUID.random()).string; + }, beforeCreate: async (user) => { user.password = await hashPassword(user.password); },