From 0a983d0654ef436700f1be5b3a5ae7309e010538 Mon Sep 17 00:00:00 2001 From: seaznCode Date: Fri, 16 Jan 2026 00:11:09 +0100 Subject: [PATCH 1/4] feat: implement SQL dump import functionality with multi-statement support --- controller/dev/DevManagementController.js | 34 +++++++++++++++++++++++ database/database.js | 7 +++++ routes/postRoutes.js | 5 ++++ services/dev/DevManagementService.js | 15 ++++++++++ 4 files changed, 61 insertions(+) create mode 100644 controller/dev/DevManagementController.js create mode 100644 services/dev/DevManagementService.js diff --git a/controller/dev/DevManagementController.js b/controller/dev/DevManagementController.js new file mode 100644 index 0000000..e3b7914 --- /dev/null +++ b/controller/dev/DevManagementController.js @@ -0,0 +1,34 @@ +const DevManagementService = require('../../services/dev/DevManagementService'); +const { logger } = require('../../middleware/logger'); + +function previewSql(sql, max = 180) { + if (!sql) return ''; + const s = String(sql).replace(/\s+/g, ' ').trim(); + return s.length > max ? `${s.slice(0, max)}â€Ļ` : s; +} + +exports.importSqlDump = async (req, res) => { + try { + if (!req.file || !req.file.buffer) { + return res.status(400).json({ success: false, error: 'SQL dump file is required' }); + } + const sql = req.file.buffer.toString('utf8'); + if (!sql.trim()) { + return res.status(400).json({ success: false, error: 'SQL dump file is empty' }); + } + const start = Date.now(); + const { rows, fields } = await DevManagementService.executeDump(sql); + const durationMs = Date.now() - start; + + const isMulti = Array.isArray(fields) && fields.length > 0 && Array.isArray(fields[0]); + + return res.json({ + success: true, + data: { result: rows, isMulti: !!isMulti }, + meta: { durationMs } + }); + } catch (e) { + logger.error('[DevManagementController.importSqlDump] error', { msg: e?.message, sql: previewSql(req.file?.buffer?.toString('utf8')) }); + return res.status(500).json({ success: false, error: e?.message || 'SQL execution failed' }); + } +}; diff --git a/database/database.js b/database/database.js index dd5284c..9f628d5 100644 --- a/database/database.js +++ b/database/database.js @@ -99,5 +99,12 @@ module.exports = { // Get a connection from the pool async getConnection() { return await pool.getConnection(); + }, + // Get a dedicated connection that allows multiple statements (for SQL dumps) + async getMultiStatementConnection() { + return await mysql.createConnection({ + ...dbConfig, + multipleStatements: true + }); } }; \ No newline at end of file diff --git a/routes/postRoutes.js b/routes/postRoutes.js index c4b214d..c37290b 100644 --- a/routes/postRoutes.js +++ b/routes/postRoutes.js @@ -28,6 +28,7 @@ const AffiliateController = require('../controller/affiliate/AffiliateController const AbonemmentController = require('../controller/abonemments/AbonemmentController'); const NewsController = require('../controller/news/NewsController'); const InvoiceController = require('../controller/invoice/InvoiceController'); // NEW +const DevManagementController = require('../controller/dev/DevManagementController'); const multer = require('multer'); const upload = multer({ storage: multer.memoryStorage() }); @@ -113,6 +114,7 @@ function adminOnly(req, res, next) { next(); } + // NEW: ensure service sees a "company" user_type for admin users function forceCompanyForAdmin(req, res, next) { if (req.user && ['admin','super_admin'].includes(req.user.role) && req.user.user_type !== 'company') { @@ -161,6 +163,9 @@ router.post('/admin/affiliates', authMiddleware, adminOnly, upload.single('logo' // NEW: Admin create news with image upload router.post('/admin/news', authMiddleware, adminOnly, upload.single('image'), NewsController.create); +// NEW: Dev Management SQL dump import (admin + super_admin) +router.post('/admin/dev/sql', authMiddleware, adminOnly, upload.single('file'), DevManagementController.importSqlDump); + // Abonement POSTs router.post('/abonements/subscribe', authMiddleware, AbonemmentController.subscribe); router.post('/abonements/:id/pause', authMiddleware, AbonemmentController.pause); diff --git a/services/dev/DevManagementService.js b/services/dev/DevManagementService.js new file mode 100644 index 0000000..f80598b --- /dev/null +++ b/services/dev/DevManagementService.js @@ -0,0 +1,15 @@ +const db = require('../../database/database'); + +async function executeDump(sql) { + const conn = await db.getMultiStatementConnection(); + try { + const [rows, fields] = await conn.query(sql); + return { rows, fields }; + } finally { + try { await conn.end(); } catch (_) {} + } +} + +module.exports = { + executeDump +}; From 1685df83893cfff1184cf97630fbb1aaca185bd4 Mon Sep 17 00:00:00 2001 From: seaznCode Date: Sat, 17 Jan 2026 19:38:49 +0100 Subject: [PATCH 2/4] feat: enhance database initialization with nullable columns and virtual labels for referral tokens --- database/createDb.js | 110 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 96 insertions(+), 14 deletions(-) diff --git a/database/createDb.js b/database/createDb.js index ef6fb02..d9b250f 100644 --- a/database/createDb.js +++ b/database/createDb.js @@ -54,7 +54,7 @@ if (NODE_ENV === 'development') { const allowCreateDb = String(process.env.DB_ALLOW_CREATE_DB || 'false').toLowerCase() === 'true'; -// --- Performance Helpers (added) --- +// --- Performance Helpers --- async function ensureIndex(conn, table, indexName, indexDDL) { const [rows] = await conn.query(`SHOW INDEX FROM \`${table}\` WHERE Key_name = ?`, [indexName]); if (!rows.length) { @@ -167,6 +167,23 @@ async function createDatabase() { `); console.log('✅ Company profiles table created/verified'); + // Ensure registration_number allows NULL if table already existed + try { + await connection.query(`ALTER TABLE company_profiles MODIFY COLUMN registration_number VARCHAR(255) UNIQUE NULL`); + console.log('🔧 Ensured registration_number allows NULL'); + } catch (e) { + console.log('â„šī¸ registration_number column already allows NULL or ALTER not required'); + } + + // Ensure phone columns are nullable if tables already existed + try { + await connection.query(`ALTER TABLE personal_profiles MODIFY COLUMN phone VARCHAR(255) NULL`); + await connection.query(`ALTER TABLE company_profiles MODIFY COLUMN phone VARCHAR(255) NULL`); + console.log('🔧 Ensured phone columns are nullable'); + } catch (e) { + console.log('â„šī¸ Phone columns already nullable or ALTER not required'); + } + // 4. user_status table: Comprehensive tracking of user verification and completion steps await connection.query(` CREATE TABLE IF NOT EXISTS user_status ( @@ -481,6 +498,43 @@ async function createDatabase() { console.log('✅ News table created/verified'); console.log('✅ Referral tokens table created/verified'); + // Generated label columns (virtual) to display 'unlimited' instead of -1 in UI tools + try { + await connection.query(` + ALTER TABLE referral_tokens + ADD COLUMN max_uses_label VARCHAR(20) + GENERATED ALWAYS AS (CASE WHEN max_uses = -1 THEN 'unlimited' ELSE CAST(max_uses AS CHAR) END) VIRTUAL + `); + console.log('🆕 Added virtual column referral_tokens.max_uses_label'); + } catch (e) { + console.log('â„šī¸ max_uses_label already exists or cannot add:', e.message); + } + try { + await connection.query(` + ALTER TABLE referral_tokens + ADD COLUMN uses_remaining_label VARCHAR(20) + GENERATED ALWAYS AS (CASE WHEN uses_remaining = -1 THEN 'unlimited' ELSE CAST(uses_remaining AS CHAR) END) VIRTUAL + `); + console.log('🆕 Added virtual column referral_tokens.uses_remaining_label'); + } catch (e) { + console.log('â„šī¸ uses_remaining_label already exists or cannot add:', e.message); + } + + // Normalized view now sources the generated label columns (still handy for joins) + try { + await connection.query(` + CREATE OR REPLACE VIEW referral_tokens_normalized AS + SELECT + rt.*, + rt.max_uses_label AS max_uses_display, + rt.uses_remaining_label AS uses_remaining_display + FROM referral_tokens rt; + `); + console.log('🆕 referral_tokens_normalized view created/updated'); + } catch (e) { + console.warn('âš ī¸ Could not create referral_tokens_normalized view:', e.message); + } + // 13. referral_token_usage table: Tracks each use of a referral token await connection.query(` CREATE TABLE IF NOT EXISTS referral_token_usage ( @@ -911,19 +965,6 @@ async function createDatabase() { `); console.log('✅ matrix_instances table created/verified'); - // Legacy: ensure legacy matrix_config exists (still needed short-term for migration) - await connection.query(` - CREATE TABLE IF NOT EXISTS matrix_config ( - id TINYINT PRIMARY KEY DEFAULT 1, - master_top_user_id INT NOT NULL, - name VARCHAR(255) NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - CONSTRAINT fk_matrix_config_master FOREIGN KEY (master_top_user_id) REFERENCES users(id) ON DELETE RESTRICT ON UPDATE CASCADE - ); - `); - console.log('â„šī¸ matrix_config (legacy) verified'); - await connection.query(` CREATE TABLE IF NOT EXISTS pools ( id INT AUTO_INCREMENT PRIMARY KEY, @@ -1080,6 +1121,47 @@ async function createDatabase() { console.log('â„šī¸ chk_matrix_singleton drop skipped:', e.message); } + // --- Added Index Optimization Section --- + try { + // Core / status + await ensureIndex(connection, 'users', 'idx_users_created_at', 'created_at'); + await ensureIndex(connection, 'user_status', 'idx_user_status_status', 'status'); + await ensureIndex(connection, 'user_status', 'idx_user_status_registration_completed', 'registration_completed'); + + // Tokens & auth + await ensureIndex(connection, 'refresh_tokens', 'idx_refresh_user_expires', 'user_id, expires_at'); + await ensureIndex(connection, 'email_verifications', 'idx_email_verifications_user_expires', 'user_id, expires_at'); + await ensureIndex(connection, 'password_resets', 'idx_password_resets_user_expires', 'user_id, expires_at'); + + // Documents + await ensureIndex(connection, 'user_documents', 'idx_user_documents_upload_at', 'upload_at'); + await ensureIndex(connection, 'user_documents', 'idx_user_documents_verified', 'verified_by_admin'); + await ensureIndex(connection, 'user_id_documents', 'idx_user_id_docs_user_type', 'user_id, document_type'); + + // Activity logs (composite for common filtered ordering) + await ensureIndex(connection, 'user_action_logs', 'idx_user_action_logs_action_created', 'action, created_at'); + + // Referrals + await ensureIndex(connection, 'referral_token_usage', 'idx_referral_token_usage_used_at', 'used_at'); + + // Permissions + await ensureIndex(connection, 'permissions', 'idx_permissions_is_active', 'is_active'); + await ensureIndex(connection, 'user_permissions', 'idx_user_permissions_granted_by', 'granted_by'); + + // Settings + await ensureIndex(connection, 'user_settings', 'idx_user_settings_theme', 'theme'); + + // Rate limit (for queries only on rate_key) + await ensureIndex(connection, 'rate_limit', 'idx_rate_limit_rate_key', 'rate_key'); + await ensureIndex(connection, 'document_templates', 'idx_document_templates_user_type', 'user_type'); + await ensureIndex(connection, 'document_templates', 'idx_document_templates_state_user_type', 'state, user_type'); + await ensureIndex(connection, 'company_stamps', 'idx_company_stamps_company', 'company_id'); + await ensureIndex(connection, 'company_stamps', 'idx_company_stamps_active', 'is_active'); + console.log('🚀 Performance indexes created/verified'); + } catch (e) { + console.warn('âš ī¸ Index optimization phase encountered an issue:', e.message); + } + console.log('🎉 Normalized database schema created/updated successfully!'); await connection.end(); From ffe44610164e88534d98d981e15e91c0ca0cb2d3 Mon Sep 17 00:00:00 2001 From: seaznCode Date: Sat, 17 Jan 2026 20:06:09 +0100 Subject: [PATCH 3/4] feat: enforce admin-only access for user management routes --- routes/deleteRoutes.js | 2 +- routes/getRoutes.js | 2 +- routes/postRoutes.js | 4 ++-- routes/putRoutes.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/routes/deleteRoutes.js b/routes/deleteRoutes.js index ebba7fd..bce2ff9 100644 --- a/routes/deleteRoutes.js +++ b/routes/deleteRoutes.js @@ -24,7 +24,7 @@ function forceCompanyForAdmin(req, res, next) { } // DELETE /admin/user/:id (moved from routes/admin.js) -router.delete('/admin/user/:id', authMiddleware, AdminUserController.deleteUser); +router.delete('/admin/user/:id', authMiddleware, adminOnly, AdminUserController.deleteUser); // DELETE /document-templates/:id (moved from routes/documentTemplates.js) router.delete('/document-templates/:id', authMiddleware, DocumentTemplateController.deleteTemplate); diff --git a/routes/getRoutes.js b/routes/getRoutes.js index 2d18c6b..faee98e 100644 --- a/routes/getRoutes.js +++ b/routes/getRoutes.js @@ -54,7 +54,7 @@ router.get('/user/status-progress', authMiddleware, UserStatusController.getStat router.get('/users/:id/full', authMiddleware, UserController.getFullUserData); router.get('/user/settings', authMiddleware, UserSettingsController.getSettings); router.get('/users/:id/permissions', authMiddleware, PermissionController.getUserPermissions); -router.get('/admin/users/:id/full', authMiddleware, AdminUserController.getFullUserAccountDetails); +router.get('/admin/users/:id/full', authMiddleware, adminOnly, AdminUserController.getFullUserAccountDetails); router.get('/admin/users/:id/detailed', authMiddleware, requireAdmin, AdminUserController.getDetailedUserInfo); router.get('/users/:id/documents', authMiddleware, UserController.getUserDocumentsAndContracts); router.get('/verify-password-reset', (req, res) => { /* Note: was moved from PasswordResetController.verifyPasswordResetToken */ res.status(204).end(); }); // keep placeholder if controller already registered via other verb diff --git a/routes/postRoutes.js b/routes/postRoutes.js index c37290b..3cc7f96 100644 --- a/routes/postRoutes.js +++ b/routes/postRoutes.js @@ -73,8 +73,8 @@ router.post('/profile/personal/complete', authMiddleware, PersonalProfileControl router.post('/profile/company/complete', authMiddleware, CompanyProfileController.completeProfile); // Admin POSTs (moved from routes/admin.js) -router.post('/admin/verify-user/:id', authMiddleware, AdminUserController.verifyUser); -router.post('/admin/send-password-reset/:userId', authMiddleware, async (req, res) => { +router.post('/admin/verify-user/:id', authMiddleware, adminOnly, AdminUserController.verifyUser); +router.post('/admin/send-password-reset/:userId', authMiddleware, adminOnly, async (req, res) => { const userId = req.params.userId; // require here to avoid circular/top-level ordering issues const UnitOfWork = require('../database/UnitOfWork'); diff --git a/routes/putRoutes.js b/routes/putRoutes.js index 3142928..c25b4d6 100644 --- a/routes/putRoutes.js +++ b/routes/putRoutes.js @@ -17,7 +17,7 @@ function adminOnly(req, res, next) { } // PUT /admin/users/:id/permissions (moved from routes/admin.js) -router.put('/admin/users/:id/permissions', authMiddleware, AdminUserController.updateUserPermissions); +router.put('/admin/users/:id/permissions', authMiddleware, adminOnly, AdminUserController.updateUserPermissions); // PUT /document-templates/:id (moved from routes/documentTemplates.js) router.put('/document-templates/:id', authMiddleware, upload.single('file'), DocumentTemplateController.updateTemplate); From f475242f847016eb0c5ad238729f0205a3792455 Mon Sep 17 00:00:00 2001 From: seaznCode Date: Sat, 17 Jan 2026 20:22:31 +0100 Subject: [PATCH 4/4] feat: update user status initialization to use 'pending' instead of 'inactive' --- controller/auth/UserStatusController.js | 2 +- repositories/status/UserStatusRepository.js | 5 +++-- scripts/createCompanyUser.js | 2 +- scripts/createPersonalUser.js | 2 +- services/admin/AdminService.js | 9 +++++++-- services/referral/ReferralService.js | 4 ++-- services/status/UserStatusService.js | 2 +- services/user/company/CompanyUserService.js | 2 +- services/user/personal/PersonalUserService.js | 2 +- 9 files changed, 18 insertions(+), 12 deletions(-) diff --git a/controller/auth/UserStatusController.js b/controller/auth/UserStatusController.js index a2affde..4c0711c 100644 --- a/controller/auth/UserStatusController.js +++ b/controller/auth/UserStatusController.js @@ -17,7 +17,7 @@ class UserStatusController { // If no status exists, create one with default values if (!status) { logger.info(`[UserStatusController] No status found for userId: ${userId}, creating default status`); - await userStatusRepo.initializeUserStatus(userId, 'inactive'); + await userStatusRepo.initializeUserStatus(userId, 'pending'); status = await userStatusRepo.getStatusByUserId(userId); } diff --git a/repositories/status/UserStatusRepository.js b/repositories/status/UserStatusRepository.js index f61ebe6..6f2f3e7 100644 --- a/repositories/status/UserStatusRepository.js +++ b/repositories/status/UserStatusRepository.js @@ -18,7 +18,7 @@ class UserStatusRepository { } } - async initializeUserStatus(userId, status = 'inactive') { + async initializeUserStatus(userId, status = 'pending') { logger.info('UserStatusRepository.initializeUserStatus:start', { userId, status }); try { const conn = this.unitOfWork.connection; @@ -57,7 +57,8 @@ class UserStatusRepository { const conn = this.unitOfWork.connection; await conn.query( `UPDATE user_status - SET registration_completed = TRUE, status = 'active' + SET registration_completed = TRUE, + status = CASE WHEN is_admin_verified = 1 THEN 'active' ELSE 'pending' END WHERE user_id = ?`, [userId] ); diff --git a/scripts/createCompanyUser.js b/scripts/createCompanyUser.js index 6a11ab3..bc9714f 100644 --- a/scripts/createCompanyUser.js +++ b/scripts/createCompanyUser.js @@ -46,7 +46,7 @@ async function createCompanyUser() { // Initialize user status (defaults: not verified, not completed) await uow.connection.query( `INSERT INTO user_status (user_id, status, is_admin_verified, email_verified, profile_completed, documents_uploaded, contract_signed) - VALUES (?, 'active', 0, 0, 0, 0, 0)`, + VALUES (?, 'pending', 0, 0, 0, 0, 0)`, [userId] ); diff --git a/scripts/createPersonalUser.js b/scripts/createPersonalUser.js index 20092b2..9eb746d 100644 --- a/scripts/createPersonalUser.js +++ b/scripts/createPersonalUser.js @@ -45,7 +45,7 @@ async function createPersonalUser() { // Initialize user status await uow.connection.query( `INSERT INTO user_status (user_id, status, is_admin_verified, email_verified, profile_completed, documents_uploaded, contract_signed) - VALUES (?, 'active', 0, 0, 0, 0, 0)`, + VALUES (?, 'pending', 0, 0, 0, 0, 0)`, [userId] ); diff --git a/services/admin/AdminService.js b/services/admin/AdminService.js index ffe56ac..7bb55ec 100644 --- a/services/admin/AdminService.js +++ b/services/admin/AdminService.js @@ -428,9 +428,14 @@ class AdminService { await unitOfWork.connection.query( `UPDATE user_status SET is_admin_verified = ?, - admin_verified_at = ? + admin_verified_at = ?, + status = CASE + WHEN status IN ('suspended','archived') THEN status + WHEN ? = 1 THEN 'active' + ELSE 'pending' + END WHERE user_id = ?`, - [isAdminVerified, timestamp, userId] + [isAdminVerified, timestamp, isAdminVerified, userId] ); logger.info('AdminService.updateUserVerification:success', { userId, isAdminVerified }); diff --git a/services/referral/ReferralService.js b/services/referral/ReferralService.js index b077927..efa979d 100644 --- a/services/referral/ReferralService.js +++ b/services/referral/ReferralService.js @@ -431,7 +431,7 @@ class ReferralService { const personalRepo = new PersonalUserRepository(unitOfWork); const user = await personalRepo.create(registrationData); - await UserStatusService.initializeUserStatus(user.id, 'personal', unitOfWork, 'inactive'); + await UserStatusService.initializeUserStatus(user.id, 'personal', unitOfWork, 'pending'); if (UserSettingsRepository) { const settingsRepo = new UserSettingsRepository(unitOfWork); @@ -479,7 +479,7 @@ class ReferralService { contactPersonPhone }); - await UserStatusService.initializeUserStatus(user.id, 'company', unitOfWork, 'inactive'); + await UserStatusService.initializeUserStatus(user.id, 'company', unitOfWork, 'pending'); if (UserSettingsRepository) { const settingsRepo = new UserSettingsRepository(unitOfWork); diff --git a/services/status/UserStatusService.js b/services/status/UserStatusService.js index c8ae32c..c51ce49 100644 --- a/services/status/UserStatusService.js +++ b/services/status/UserStatusService.js @@ -2,7 +2,7 @@ const UserStatusRepository = require('../../repositories/status/UserStatusReposi const { logger } = require('../../middleware/logger'); class UserStatusService { - static async initializeUserStatus(userId, userType, unitOfWork, status = 'inactive') { + static async initializeUserStatus(userId, userType, unitOfWork, status = 'pending') { logger.info('UserStatusService.initializeUserStatus:start', { userId, userType, status }); try { const repo = new UserStatusRepository(unitOfWork); diff --git a/services/user/company/CompanyUserService.js b/services/user/company/CompanyUserService.js index a7bf5f0..3447753 100644 --- a/services/user/company/CompanyUserService.js +++ b/services/user/company/CompanyUserService.js @@ -31,7 +31,7 @@ class CompanyUserService { logger.info('CompanyUserService.createCompanyUser:company_created', { companyId: newCompany.id }); // Initialize user status - await UserStatusService.initializeUserStatus(newCompany.id, 'company', unitOfWork, 'inactive'); + await UserStatusService.initializeUserStatus(newCompany.id, 'company', unitOfWork, 'pending'); logger.info('CompanyUserService.createCompanyUser:user_status_initialized', { companyId: newCompany.id }); // Send registration email to the new company user diff --git a/services/user/personal/PersonalUserService.js b/services/user/personal/PersonalUserService.js index 468f324..9c1e31b 100644 --- a/services/user/personal/PersonalUserService.js +++ b/services/user/personal/PersonalUserService.js @@ -31,7 +31,7 @@ class PersonalUserService { logger.info('PersonalUserService.createPersonalUser:user_created', { userId: newUser.id }); // Initialize user status - await UserStatusService.initializeUserStatus(newUser.id, 'personal', unitOfWork, 'inactive'); + await UserStatusService.initializeUserStatus(newUser.id, 'personal', unitOfWork, 'pending'); logger.info('PersonalUserService.createPersonalUser:user_status_initialized', { userId: newUser.id }); // Handle referral if provided