This commit is contained in:
DeathKaioken 2026-01-18 16:19:11 +01:00
commit e86986d40c
17 changed files with 180 additions and 31 deletions

View File

@ -17,7 +17,7 @@ class UserStatusController {
// If no status exists, create one with default values // If no status exists, create one with default values
if (!status) { if (!status) {
logger.info(`[UserStatusController] No status found for userId: ${userId}, creating default 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); status = await userStatusRepo.getStatusByUserId(userId);
} }

View File

@ -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' });
}
};

View File

@ -54,7 +54,7 @@ if (NODE_ENV === 'development') {
const allowCreateDb = String(process.env.DB_ALLOW_CREATE_DB || 'false').toLowerCase() === 'true'; const allowCreateDb = String(process.env.DB_ALLOW_CREATE_DB || 'false').toLowerCase() === 'true';
// --- Performance Helpers (added) --- // --- Performance Helpers ---
async function ensureIndex(conn, table, indexName, indexDDL) { async function ensureIndex(conn, table, indexName, indexDDL) {
const [rows] = await conn.query(`SHOW INDEX FROM \`${table}\` WHERE Key_name = ?`, [indexName]); const [rows] = await conn.query(`SHOW INDEX FROM \`${table}\` WHERE Key_name = ?`, [indexName]);
if (!rows.length) { if (!rows.length) {
@ -225,6 +225,23 @@ const createDatabase = async () => {
`); `);
console.log('✅ Company profiles table created/verified'); 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 // 4. user_status table: Comprehensive tracking of user verification and completion steps
await connection.query(` await connection.query(`
CREATE TABLE IF NOT EXISTS user_status ( CREATE TABLE IF NOT EXISTS user_status (
@ -527,6 +544,43 @@ const createDatabase = async () => {
console.log('✅ News table created/verified'); console.log('✅ News table created/verified');
console.log('✅ Referral tokens 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 // 13. referral_token_usage table: Tracks each use of a referral token
await connection.query(` await connection.query(`
CREATE TABLE IF NOT EXISTS referral_token_usage ( CREATE TABLE IF NOT EXISTS referral_token_usage (
@ -942,19 +996,6 @@ const createDatabase = async () => {
`); `);
console.log('✅ matrix_instances table created/verified'); 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(` await connection.query(`
CREATE TABLE IF NOT EXISTS pools ( CREATE TABLE IF NOT EXISTS pools (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
@ -1134,6 +1175,47 @@ const createDatabase = async () => {
console.log(' chk_matrix_singleton drop skipped:', e.message); 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!'); console.log('🎉 Normalized database schema created/updated successfully!');
await connection.end(); await connection.end();

View File

@ -99,5 +99,12 @@ module.exports = {
// Get a connection from the pool // Get a connection from the pool
async getConnection() { async getConnection() {
return await pool.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
});
} }
}; };

View File

@ -18,7 +18,7 @@ class UserStatusRepository {
} }
} }
async initializeUserStatus(userId, status = 'inactive') { async initializeUserStatus(userId, status = 'pending') {
logger.info('UserStatusRepository.initializeUserStatus:start', { userId, status }); logger.info('UserStatusRepository.initializeUserStatus:start', { userId, status });
try { try {
const conn = this.unitOfWork.connection; const conn = this.unitOfWork.connection;
@ -57,7 +57,8 @@ class UserStatusRepository {
const conn = this.unitOfWork.connection; const conn = this.unitOfWork.connection;
await conn.query( await conn.query(
`UPDATE user_status `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 = ?`, WHERE user_id = ?`,
[userId] [userId]
); );

View File

@ -24,7 +24,7 @@ function forceCompanyForAdmin(req, res, next) {
} }
// DELETE /admin/user/:id (moved from routes/admin.js) // 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) // DELETE /document-templates/:id (moved from routes/documentTemplates.js)
router.delete('/document-templates/:id', authMiddleware, DocumentTemplateController.deleteTemplate); router.delete('/document-templates/:id', authMiddleware, DocumentTemplateController.deleteTemplate);

View File

@ -54,7 +54,7 @@ router.get('/user/status-progress', authMiddleware, UserStatusController.getStat
router.get('/users/:id/full', authMiddleware, UserController.getFullUserData); router.get('/users/:id/full', authMiddleware, UserController.getFullUserData);
router.get('/user/settings', authMiddleware, UserSettingsController.getSettings); router.get('/user/settings', authMiddleware, UserSettingsController.getSettings);
router.get('/users/:id/permissions', authMiddleware, PermissionController.getUserPermissions); 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('/admin/users/:id/detailed', authMiddleware, requireAdmin, AdminUserController.getDetailedUserInfo);
router.get('/users/:id/documents', authMiddleware, UserController.getUserDocumentsAndContracts); 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 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

View File

@ -28,6 +28,7 @@ const AffiliateController = require('../controller/affiliate/AffiliateController
const AbonemmentController = require('../controller/abonemments/AbonemmentController'); const AbonemmentController = require('../controller/abonemments/AbonemmentController');
const NewsController = require('../controller/news/NewsController'); const NewsController = require('../controller/news/NewsController');
const InvoiceController = require('../controller/invoice/InvoiceController'); // NEW const InvoiceController = require('../controller/invoice/InvoiceController'); // NEW
const DevManagementController = require('../controller/dev/DevManagementController');
const multer = require('multer'); const multer = require('multer');
const upload = multer({ storage: multer.memoryStorage() }); const upload = multer({ storage: multer.memoryStorage() });
@ -73,8 +74,8 @@ router.post('/profile/personal/complete', authMiddleware, PersonalProfileControl
router.post('/profile/company/complete', authMiddleware, CompanyProfileController.completeProfile); router.post('/profile/company/complete', authMiddleware, CompanyProfileController.completeProfile);
// Admin POSTs (moved from routes/admin.js) // Admin POSTs (moved from routes/admin.js)
router.post('/admin/verify-user/:id', authMiddleware, AdminUserController.verifyUser); router.post('/admin/verify-user/:id', authMiddleware, adminOnly, AdminUserController.verifyUser);
router.post('/admin/send-password-reset/:userId', authMiddleware, async (req, res) => { router.post('/admin/send-password-reset/:userId', authMiddleware, adminOnly, async (req, res) => {
const userId = req.params.userId; const userId = req.params.userId;
// require here to avoid circular/top-level ordering issues // require here to avoid circular/top-level ordering issues
const UnitOfWork = require('../database/UnitOfWork'); const UnitOfWork = require('../database/UnitOfWork');
@ -114,6 +115,7 @@ function adminOnly(req, res, next) {
next(); next();
} }
// NEW: ensure service sees a "company" user_type for admin users // NEW: ensure service sees a "company" user_type for admin users
function forceCompanyForAdmin(req, res, next) { function forceCompanyForAdmin(req, res, next) {
if (req.user && ['admin','super_admin'].includes(req.user.role) && req.user.user_type !== 'company') { if (req.user && ['admin','super_admin'].includes(req.user.role) && req.user.user_type !== 'company') {
@ -162,6 +164,9 @@ router.post('/admin/affiliates', authMiddleware, adminOnly, upload.single('logo'
// NEW: Admin create news with image upload // NEW: Admin create news with image upload
router.post('/admin/news', authMiddleware, adminOnly, upload.single('image'), NewsController.create); 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 // Abonement POSTs
router.post('/abonements/subscribe', authMiddleware, AbonemmentController.subscribe); router.post('/abonements/subscribe', authMiddleware, AbonemmentController.subscribe);
router.post('/abonements/:id/pause', authMiddleware, AbonemmentController.pause); router.post('/abonements/:id/pause', authMiddleware, AbonemmentController.pause);

View File

@ -17,7 +17,7 @@ function adminOnly(req, res, next) {
} }
// PUT /admin/users/:id/permissions (moved from routes/admin.js) // 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) // PUT /document-templates/:id (moved from routes/documentTemplates.js)
router.put('/document-templates/:id', authMiddleware, upload.single('file'), DocumentTemplateController.updateTemplate); router.put('/document-templates/:id', authMiddleware, upload.single('file'), DocumentTemplateController.updateTemplate);

View File

@ -46,7 +46,7 @@ async function createCompanyUser() {
// Initialize user status (defaults: not verified, not completed) // Initialize user status (defaults: not verified, not completed)
await uow.connection.query( await uow.connection.query(
`INSERT INTO user_status (user_id, status, is_admin_verified, email_verified, profile_completed, documents_uploaded, contract_signed) `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] [userId]
); );

View File

@ -45,7 +45,7 @@ async function createPersonalUser() {
// Initialize user status // Initialize user status
await uow.connection.query( await uow.connection.query(
`INSERT INTO user_status (user_id, status, is_admin_verified, email_verified, profile_completed, documents_uploaded, contract_signed) `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] [userId]
); );

View File

@ -428,9 +428,14 @@ class AdminService {
await unitOfWork.connection.query( await unitOfWork.connection.query(
`UPDATE user_status `UPDATE user_status
SET is_admin_verified = ?, 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 = ?`, WHERE user_id = ?`,
[isAdminVerified, timestamp, userId] [isAdminVerified, timestamp, isAdminVerified, userId]
); );
logger.info('AdminService.updateUserVerification:success', { userId, isAdminVerified }); logger.info('AdminService.updateUserVerification:success', { userId, isAdminVerified });

View File

@ -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
};

View File

@ -431,7 +431,7 @@ class ReferralService {
const personalRepo = new PersonalUserRepository(unitOfWork); const personalRepo = new PersonalUserRepository(unitOfWork);
const user = await personalRepo.create(registrationData); const user = await personalRepo.create(registrationData);
await UserStatusService.initializeUserStatus(user.id, 'personal', unitOfWork, 'inactive'); await UserStatusService.initializeUserStatus(user.id, 'personal', unitOfWork, 'pending');
if (UserSettingsRepository) { if (UserSettingsRepository) {
const settingsRepo = new UserSettingsRepository(unitOfWork); const settingsRepo = new UserSettingsRepository(unitOfWork);
@ -479,7 +479,7 @@ class ReferralService {
contactPersonPhone contactPersonPhone
}); });
await UserStatusService.initializeUserStatus(user.id, 'company', unitOfWork, 'inactive'); await UserStatusService.initializeUserStatus(user.id, 'company', unitOfWork, 'pending');
if (UserSettingsRepository) { if (UserSettingsRepository) {
const settingsRepo = new UserSettingsRepository(unitOfWork); const settingsRepo = new UserSettingsRepository(unitOfWork);

View File

@ -2,7 +2,7 @@ const UserStatusRepository = require('../../repositories/status/UserStatusReposi
const { logger } = require('../../middleware/logger'); const { logger } = require('../../middleware/logger');
class UserStatusService { 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 }); logger.info('UserStatusService.initializeUserStatus:start', { userId, userType, status });
try { try {
const repo = new UserStatusRepository(unitOfWork); const repo = new UserStatusRepository(unitOfWork);

View File

@ -31,7 +31,7 @@ class CompanyUserService {
logger.info('CompanyUserService.createCompanyUser:company_created', { companyId: newCompany.id }); logger.info('CompanyUserService.createCompanyUser:company_created', { companyId: newCompany.id });
// Initialize user status // 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 }); logger.info('CompanyUserService.createCompanyUser:user_status_initialized', { companyId: newCompany.id });
// Send registration email to the new company user // Send registration email to the new company user

View File

@ -31,7 +31,7 @@ class PersonalUserService {
logger.info('PersonalUserService.createPersonalUser:user_created', { userId: newUser.id }); logger.info('PersonalUserService.createPersonalUser:user_created', { userId: newUser.id });
// Initialize user status // 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 }); logger.info('PersonalUserService.createPersonalUser:user_status_initialized', { userId: newUser.id });
// Handle referral if provided // Handle referral if provided