From 747af4db9abfa8ad8402570d4c0cd5a8b29917ab Mon Sep 17 00:00:00 2001 From: seaznCode Date: Thu, 23 Oct 2025 21:30:17 +0200 Subject: [PATCH] feat: add user management functionalities including archiving, unarchiving, verification updates, and profile updates --- controller/admin/AdminUserController.js | 80 +++++++++++++++ database/createDb.js | 17 ++++ routes/patchRoutes.js | 7 ++ services/admin/AdminService.js | 128 ++++++++++++++++++++++++ 4 files changed, 232 insertions(+) diff --git a/controller/admin/AdminUserController.js b/controller/admin/AdminUserController.js index bdb8667..2e092a1 100644 --- a/controller/admin/AdminUserController.js +++ b/controller/admin/AdminUserController.js @@ -140,6 +140,86 @@ class AdminUserController { res.status(500).json({ success: false, message: error.message }); } } + + static async archiveUser(req, res) { + if (!req.user || (req.user.role !== 'admin' && req.user.role !== 'super_admin')) { + return res.status(403).json({ success: false, message: 'Forbidden: Admins only.' }); + } + const userId = req.params.id; + const unitOfWork = new UnitOfWork(); + await unitOfWork.start(); + try { + await AdminService.archiveUser(unitOfWork, userId); + await unitOfWork.commit(); + res.json({ success: true, message: 'User archived successfully.' }); + } catch (error) { + await unitOfWork.rollback(error); + res.status(500).json({ success: false, message: error.message }); + } + } + + static async unarchiveUser(req, res) { + if (!req.user || (req.user.role !== 'admin' && req.user.role !== 'super_admin')) { + return res.status(403).json({ success: false, message: 'Forbidden: Admins only.' }); + } + const userId = req.params.id; + const unitOfWork = new UnitOfWork(); + await unitOfWork.start(); + try { + await AdminService.unarchiveUser(unitOfWork, userId); + await unitOfWork.commit(); + res.json({ success: true, message: 'User unarchived successfully.' }); + } catch (error) { + await unitOfWork.rollback(error); + res.status(500).json({ success: false, message: error.message }); + } + } + + static async updateUserVerification(req, res) { + if (!req.user || (req.user.role !== 'admin' && req.user.role !== 'super_admin')) { + return res.status(403).json({ success: false, message: 'Forbidden: Admins only.' }); + } + const userId = req.params.id; + const { is_admin_verified } = req.body; + + if (typeof is_admin_verified !== 'number' || (is_admin_verified !== 0 && is_admin_verified !== 1)) { + return res.status(400).json({ success: false, message: 'Invalid is_admin_verified value. Must be 0 or 1.' }); + } + + const unitOfWork = new UnitOfWork(); + await unitOfWork.start(); + try { + await AdminService.updateUserVerification(unitOfWork, userId, is_admin_verified); + await unitOfWork.commit(); + res.json({ success: true, message: 'User verification status updated successfully.' }); + } catch (error) { + await unitOfWork.rollback(error); + res.status(500).json({ success: false, message: error.message }); + } + } + + static async updateUserProfile(req, res) { + if (!req.user || (req.user.role !== 'admin' && req.user.role !== 'super_admin')) { + return res.status(403).json({ success: false, message: 'Forbidden: Admins only.' }); + } + const userId = req.params.id; + const { profileData, userType } = req.body; + + if (!profileData || !userType) { + return res.status(400).json({ success: false, message: 'Missing profileData or userType.' }); + } + + const unitOfWork = new UnitOfWork(); + await unitOfWork.start(); + try { + await AdminService.updateUserProfile(unitOfWork, userId, profileData, userType); + await unitOfWork.commit(); + res.json({ success: true, message: 'User profile updated successfully.' }); + } catch (error) { + await unitOfWork.rollback(error); + res.status(500).json({ success: false, message: error.message }); + } + } } module.exports = AdminUserController; \ No newline at end of file diff --git a/database/createDb.js b/database/createDb.js index a58170a..06ad6df 100644 --- a/database/createDb.js +++ b/database/createDb.js @@ -189,6 +189,7 @@ async function createDatabase() { CREATE TABLE IF NOT EXISTS user_status ( user_id INT PRIMARY KEY, status ENUM('inactive', 'pending', 'active', 'suspended') DEFAULT 'pending', + previous_status ENUM('inactive', 'pending', 'active', 'suspended') NULL, email_verified BOOLEAN DEFAULT FALSE, email_verified_at TIMESTAMP NULL, profile_completed BOOLEAN DEFAULT FALSE, @@ -205,6 +206,22 @@ async function createDatabase() { ); `); console.log('✅ User status table created/verified'); + + // Add previous_status column if it doesn't exist (for existing databases) + try { + await connection.query(` + ALTER TABLE user_status + ADD COLUMN previous_status ENUM('inactive', 'pending', 'active', 'suspended') NULL + AFTER status + `); + console.log('✅ Added previous_status column to user_status table'); + } catch (err) { + if (err.code === 'ER_DUP_FIELDNAME') { + console.log('ℹ️ previous_status column already exists in user_status table'); + } else { + console.warn('⚠️ Could not add previous_status column:', err.message); + } + } // --- Authentication & Verification Tables --- diff --git a/routes/patchRoutes.js b/routes/patchRoutes.js index 859ccf3..c381d71 100644 --- a/routes/patchRoutes.js +++ b/routes/patchRoutes.js @@ -4,6 +4,7 @@ const router = express.Router(); const authMiddleware = require('../middleware/authMiddleware'); const DocumentTemplateController = require('../controller/documentTemplate/DocumentTemplateController'); const CompanyStampController = require('../controller/companyStamp/CompanyStampController'); // <-- added +const AdminUserController = require('../controller/admin/AdminUserController'); // Helper middlewares for company-stamp function adminOnly(req, res, next) { @@ -25,6 +26,12 @@ router.patch('/document-templates/:id/state', authMiddleware, DocumentTemplateCo // Company-stamp PATCH (activate) router.patch('/company-stamps/:id/activate', authMiddleware, adminOnly, forceCompanyForAdmin, CompanyStampController.activate); +// Admin user management PATCH routes +router.patch('/admin/archive-user/:id', authMiddleware, adminOnly, AdminUserController.archiveUser); +router.patch('/admin/unarchive-user/:id', authMiddleware, adminOnly, AdminUserController.unarchiveUser); +router.patch('/admin/update-verification/:id', authMiddleware, adminOnly, AdminUserController.updateUserVerification); +router.patch('/admin/update-user-profile/:id', authMiddleware, adminOnly, AdminUserController.updateUserProfile); + // Add other PATCH routes here as needed module.exports = router; diff --git a/services/admin/AdminService.js b/services/admin/AdminService.js index fcf329e..47b8c3c 100644 --- a/services/admin/AdminService.js +++ b/services/admin/AdminService.js @@ -298,6 +298,134 @@ class AdminService { throw error; } } + + static async archiveUser(unitOfWork, userId) { + logger.info('AdminService.archiveUser:start', { userId }); + try { + // First, get the current status to store it + const [statusRows] = await unitOfWork.connection.query( + `SELECT status FROM user_status WHERE user_id = ? LIMIT 1`, + [userId] + ); + + if (statusRows.length === 0) { + throw new Error('User status not found'); + } + + const currentStatus = statusRows[0].status; + + // Don't archive if already inactive + if (currentStatus === 'inactive') { + logger.warn('AdminService.archiveUser:already_inactive', { userId }); + return { message: 'User is already archived' }; + } + + // Store the previous status and update to inactive + await unitOfWork.connection.query( + `UPDATE user_status + SET status = 'inactive', + previous_status = ? + WHERE user_id = ?`, + [currentStatus, userId] + ); + + logger.info('AdminService.archiveUser:success', { userId, previousStatus: currentStatus }); + } catch (error) { + logger.error('AdminService.archiveUser:error', { userId, error: error.message }); + throw error; + } + } + + static async unarchiveUser(unitOfWork, userId) { + logger.info('AdminService.unarchiveUser:start', { userId }); + try { + // Get the current status and previous status + const [statusRows] = await unitOfWork.connection.query( + `SELECT status, previous_status FROM user_status WHERE user_id = ? LIMIT 1`, + [userId] + ); + + if (statusRows.length === 0) { + throw new Error('User status not found'); + } + + const currentStatus = statusRows[0].status; + const previousStatus = statusRows[0].previous_status; + + // Only unarchive if currently inactive + if (currentStatus !== 'inactive') { + logger.warn('AdminService.unarchiveUser:not_inactive', { userId, currentStatus }); + return { message: 'User is not archived' }; + } + + // Restore to previous status or default to 'pending' if no previous status exists + const restoreStatus = previousStatus || 'pending'; + + await unitOfWork.connection.query( + `UPDATE user_status + SET status = ?, + previous_status = NULL + WHERE user_id = ?`, + [restoreStatus, userId] + ); + + logger.info('AdminService.unarchiveUser:success', { userId, restoredStatus: restoreStatus }); + } catch (error) { + logger.error('AdminService.unarchiveUser:error', { userId, error: error.message }); + throw error; + } + } + + static async updateUserVerification(unitOfWork, userId, isAdminVerified) { + logger.info('AdminService.updateUserVerification:start', { userId, isAdminVerified }); + try { + // Update the is_admin_verified field in user_status + const timestamp = isAdminVerified === 1 ? new Date() : null; + + await unitOfWork.connection.query( + `UPDATE user_status + SET is_admin_verified = ?, + admin_verified_at = ? + WHERE user_id = ?`, + [isAdminVerified, timestamp, userId] + ); + + logger.info('AdminService.updateUserVerification:success', { userId, isAdminVerified }); + } catch (error) { + logger.error('AdminService.updateUserVerification:error', { userId, error: error.message }); + throw error; + } + } + + static async updateUserProfile(unitOfWork, userId, profileData, userType) { + logger.info('AdminService.updateUserProfile:start', { userId, userType }); + try { + if (userType === 'personal') { + const { first_name, last_name, phone, date_of_birth, address, city, zip_code, country } = profileData; + await unitOfWork.connection.query( + `UPDATE personal_profiles + SET first_name = ?, last_name = ?, phone = ?, date_of_birth = ?, + address = ?, city = ?, zip_code = ?, country = ? + WHERE user_id = ?`, + [first_name, last_name, phone, date_of_birth, address, city, zip_code, country, userId] + ); + } else if (userType === 'company') { + const { company_name, registration_number, tax_id, phone, address, city, zip_code, country } = profileData; + await unitOfWork.connection.query( + `UPDATE company_profiles + SET company_name = ?, registration_number = ?, tax_id = ?, phone = ?, + address = ?, city = ?, zip_code = ?, country = ? + WHERE user_id = ?`, + [company_name, registration_number, tax_id, phone, address, city, zip_code, country, userId] + ); + } + + logger.info('AdminService.updateUserProfile:success', { userId, userType }); + } catch (error) { + logger.error('AdminService.updateUserProfile:error', { userId, error: error.message }); + throw error; + } + } } module.exports = AdminService;