feat: profile backend link | NOT DONE
This commit is contained in:
parent
b424e90e08
commit
20f69c272c
@ -3,6 +3,27 @@ const PersonalProfileService = require('../../services/profile/personal/Personal
|
|||||||
const PersonalUserRepository = require('../../repositories/user/personal/PersonalUserRepository');
|
const PersonalUserRepository = require('../../repositories/user/personal/PersonalUserRepository');
|
||||||
const { logger } = require('../../middleware/logger');
|
const { logger } = require('../../middleware/logger');
|
||||||
|
|
||||||
|
// helpers
|
||||||
|
const MAX = {
|
||||||
|
name: 100,
|
||||||
|
email: 255,
|
||||||
|
phone: 32,
|
||||||
|
address: 255,
|
||||||
|
accountHolder: 140,
|
||||||
|
iban: 34
|
||||||
|
};
|
||||||
|
const trim = v => (typeof v === 'string' ? v.trim() : v);
|
||||||
|
const isEmail = v => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
|
||||||
|
const isPhone = v => v ? /^\+?[1-9]\d{6,14}$/.test(v) : true;
|
||||||
|
const normalizeIban = v => (v || '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase(); // sanitize only, no validation
|
||||||
|
const truncate = (s, n) => (typeof s === 'string' && s.length > n ? s.slice(0, n) : s);
|
||||||
|
const maskIban = v => {
|
||||||
|
const s = normalizeIban(v || '');
|
||||||
|
if (!s) return null;
|
||||||
|
const last4 = s.slice(-4);
|
||||||
|
return `**** **** **** **** **** **** **** ${last4}`.replace(/\s{2,}/g, ' ');
|
||||||
|
};
|
||||||
|
|
||||||
class PersonalProfileController {
|
class PersonalProfileController {
|
||||||
static async completeProfile(req, res) {
|
static async completeProfile(req, res) {
|
||||||
const userId = req.user.userId;
|
const userId = req.user.userId;
|
||||||
@ -22,6 +43,201 @@ class PersonalProfileController {
|
|||||||
res.status(400).json({ success: false, message: error.message });
|
res.status(400).json({ success: false, message: error.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async updateBasic(req, res) {
|
||||||
|
if (!req.user) return res.status(401).json({ success: false, message: 'Unauthenticated' });
|
||||||
|
res.setHeader('X-Controller', 'PersonalProfileController:updateBasic'); // prove controller is hit
|
||||||
|
|
||||||
|
// prefer camelCase, then snake_case, then infer from route namespace
|
||||||
|
const inferredFromRoute = req.baseUrl?.includes('/profile/personal') || req.originalUrl?.includes('/profile/personal') ? 'personal' : undefined;
|
||||||
|
const userType = req.user.userType ?? req.user.user_type ?? inferredFromRoute;
|
||||||
|
|
||||||
|
const authDebugBasic = {
|
||||||
|
derivedUserType: userType,
|
||||||
|
reqUserKeys: Object.keys(req.user || {}),
|
||||||
|
id: req.user?.id,
|
||||||
|
userId: req.user?.userId
|
||||||
|
};
|
||||||
|
logger.info(`PersonalProfileController:updateBasic:auth userType=${userType} keys=${authDebugBasic.reqUserKeys.join(',')}`);
|
||||||
|
console.log('[PersonalProfileController:updateBasic] auth', authDebugBasic);
|
||||||
|
|
||||||
|
if (userType !== 'personal') {
|
||||||
|
const message = 'Personal user required';
|
||||||
|
const debug = process.env.NODE_ENV !== 'production'
|
||||||
|
? { expected: 'personal', got: userType, reqUserKeys: authDebugBasic.reqUserKeys }
|
||||||
|
: undefined;
|
||||||
|
return res.status(400).json({ success: false, message, ...(debug && { debug }) });
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = req.user.id || req.user.userId;
|
||||||
|
|
||||||
|
// Only take fields that are provided and non-empty after trim
|
||||||
|
let { firstName, lastName, email, phone, address } = req.body || {};
|
||||||
|
const updates = {};
|
||||||
|
|
||||||
|
if (firstName !== undefined) {
|
||||||
|
const v = truncate(trim(firstName), MAX.name);
|
||||||
|
if (v !== '') updates.firstName = v;
|
||||||
|
}
|
||||||
|
if (lastName !== undefined) {
|
||||||
|
const v = truncate(trim(lastName), MAX.name);
|
||||||
|
if (v !== '') updates.lastName = v;
|
||||||
|
}
|
||||||
|
if (email !== undefined) {
|
||||||
|
const v = truncate(trim(email), MAX.email);
|
||||||
|
if (v !== '') updates.email = v;
|
||||||
|
}
|
||||||
|
if (phone !== undefined) {
|
||||||
|
const v = truncate(trim(phone), MAX.phone);
|
||||||
|
if (v !== '') updates.phone = v;
|
||||||
|
}
|
||||||
|
if (address !== undefined) {
|
||||||
|
const v = truncate(trim(address), MAX.address);
|
||||||
|
if (v !== '') updates.address = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate only provided non-empty fields
|
||||||
|
if (updates.email && !isEmail(updates.email)) {
|
||||||
|
return res.status(400).json({ success: false, message: 'Invalid email' });
|
||||||
|
}
|
||||||
|
if (updates.phone && !isPhone(updates.phone)) {
|
||||||
|
return res.status(400).json({ success: false, message: 'Invalid phone' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const unitOfWork = new UnitOfWork();
|
||||||
|
await unitOfWork.start();
|
||||||
|
logger.info('PersonalProfileController:updateBasic:start', { userId });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updated = await PersonalProfileService.updateBasic(userId, updates, unitOfWork);
|
||||||
|
await unitOfWork.commit();
|
||||||
|
|
||||||
|
if (!updated) return res.status(404).json({ success: false, message: 'User not found' });
|
||||||
|
|
||||||
|
const response = { ...updated, iban: maskIban(updated.iban) };
|
||||||
|
logger.info('PersonalProfileController:updateBasic:success', { userId });
|
||||||
|
return res.status(200).json({ success: true, profile: response });
|
||||||
|
} catch (err) {
|
||||||
|
await unitOfWork.rollback(err);
|
||||||
|
|
||||||
|
// add explicit debug for missing user_id column
|
||||||
|
if (err?.message?.includes("Unknown column 'user_id'")) {
|
||||||
|
logger.error('PersonalProfileController:updateBasic:schemaMismatch', {
|
||||||
|
userId,
|
||||||
|
error: err.message,
|
||||||
|
sql: err.sql,
|
||||||
|
sqlState: err.sqlState,
|
||||||
|
code: err.code
|
||||||
|
});
|
||||||
|
res.setHeader('X-Error-Code', 'SCHEMA_MISMATCH_USER_ID');
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Database schema mismatch: column user_id is missing.',
|
||||||
|
debug: process.env.NODE_ENV !== 'production' ? {
|
||||||
|
hint: 'Update the table to include user_id or change the repository SQL to use the correct column name.',
|
||||||
|
repoFile: 'repositories/user/personal/PersonalUserRepository.js',
|
||||||
|
repoLine: 302
|
||||||
|
} : undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error('PersonalProfileController:updateBasic:error', { userId, error: err.message });
|
||||||
|
if (err.code === 'EMAIL_CONFLICT') {
|
||||||
|
return res.status(409).json({ success: false, message: 'Email already in use' });
|
||||||
|
}
|
||||||
|
return res.status(400).json({ success: false, message: err.message || 'Invalid input' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async updateBank(req, res) {
|
||||||
|
if (!req.user) return res.status(401).json({ success: false, message: 'Unauthenticated' });
|
||||||
|
res.setHeader('X-Controller', 'PersonalProfileController:updateBank'); // prove controller is hit
|
||||||
|
|
||||||
|
// prefer camelCase, then snake_case, then infer from route namespace
|
||||||
|
const inferredFromRoute = req.baseUrl?.includes('/profile/personal') || req.originalUrl?.includes('/profile/personal') ? 'personal' : undefined;
|
||||||
|
const userType = req.user.userType ?? req.user.user_type ?? inferredFromRoute;
|
||||||
|
|
||||||
|
const authDebugBank = {
|
||||||
|
derivedUserType: userType,
|
||||||
|
reqUserKeys: Object.keys(req.user || {}),
|
||||||
|
id: req.user?.id,
|
||||||
|
userId: req.user?.userId
|
||||||
|
};
|
||||||
|
logger.info(`PersonalProfileController:updateBank:auth userType=${userType} keys=${authDebugBank.reqUserKeys.join(',')}`);
|
||||||
|
console.log('[PersonalProfileController:updateBank] auth', authDebugBank);
|
||||||
|
|
||||||
|
if (userType !== 'personal') {
|
||||||
|
const message = 'Personal user required';
|
||||||
|
const debug = process.env.NODE_ENV !== 'production'
|
||||||
|
? { expected: 'personal', got: userType, reqUserKeys: authDebugBank.reqUserKeys }
|
||||||
|
: undefined;
|
||||||
|
return res.status(400).json({ success: false, message, ...(debug && { debug }) });
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = req.user.id || req.user.userId;
|
||||||
|
|
||||||
|
let { accountHolder, iban } = req.body || {};
|
||||||
|
const payload = {};
|
||||||
|
|
||||||
|
// IBAN debug (pre-sanitize)
|
||||||
|
const rawIban = iban ?? '';
|
||||||
|
const normalizedIban = truncate(normalizeIban(rawIban), MAX.iban);
|
||||||
|
|
||||||
|
// Log what we received vs what we will use
|
||||||
|
const ibanDebug = {
|
||||||
|
provided: iban !== undefined,
|
||||||
|
raw: rawIban,
|
||||||
|
rawLength: String(rawIban).length,
|
||||||
|
normalized: normalizedIban,
|
||||||
|
normalizedMasked: maskIban(normalizedIban),
|
||||||
|
normalizedLength: normalizedIban.length,
|
||||||
|
truncatedTo: MAX.iban
|
||||||
|
};
|
||||||
|
logger.info('PersonalProfileController:updateBank:iban_debug', { userId, ...ibanDebug });
|
||||||
|
console.log('[PersonalProfileController:updateBank] iban_debug', ibanDebug);
|
||||||
|
// Small headers for quick check in browser Network panel (avoid large payload)
|
||||||
|
res.setHeader('X-IBAN-Len', String(ibanDebug.normalizedLength));
|
||||||
|
if (ibanDebug.normalizedMasked) res.setHeader('X-IBAN-Masked', ibanDebug.normalizedMasked);
|
||||||
|
|
||||||
|
if (accountHolder !== undefined) {
|
||||||
|
const v = truncate(trim(accountHolder), MAX.accountHolder);
|
||||||
|
if (v !== '') payload.accountHolderName = v;
|
||||||
|
}
|
||||||
|
if (iban !== undefined) {
|
||||||
|
// sanitize only; do not validate IBAN format
|
||||||
|
if (normalizedIban !== '') {
|
||||||
|
payload.iban = normalizedIban;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log final payload forwarded to service (mask IBAN)
|
||||||
|
const payloadLog = {
|
||||||
|
...payload,
|
||||||
|
...(payload.iban ? { iban: maskIban(payload.iban) } : {})
|
||||||
|
};
|
||||||
|
logger.info('PersonalProfileController:updateBank:payload', { userId, payload: payloadLog });
|
||||||
|
console.log('[PersonalProfileController:updateBank] payload', payloadLog);
|
||||||
|
|
||||||
|
const unitOfWork = new UnitOfWork();
|
||||||
|
await unitOfWork.start();
|
||||||
|
logger.info('PersonalProfileController:updateBank:start', { userId });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updated = await PersonalProfileService.updateBank(userId, payload, unitOfWork);
|
||||||
|
await unitOfWork.commit();
|
||||||
|
|
||||||
|
if (!updated) return res.status(404).json({ success: false, message: 'User not found' });
|
||||||
|
|
||||||
|
const response = { ...updated, iban: maskIban(updated.iban) };
|
||||||
|
logger.info('PersonalProfileController:updateBank:success', { userId, responsePreview: { iban: response.iban, accountHolderName: response.accountHolderName } });
|
||||||
|
return res.status(200).json({ success: true, profile: response });
|
||||||
|
} catch (err) {
|
||||||
|
await unitOfWork.rollback(err);
|
||||||
|
logger.error('PersonalProfileController:updateBank:error', { userId, error: err.message });
|
||||||
|
res.setHeader('X-IBAN-Error', 'controller_catch');
|
||||||
|
return res.status(400).json({ success: false, message: err.message || 'Invalid input' });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = PersonalProfileController;
|
module.exports = PersonalProfileController;
|
||||||
|
|||||||
@ -126,19 +126,19 @@ async function createDatabase() {
|
|||||||
nationality VARCHAR(255),
|
nationality VARCHAR(255),
|
||||||
address TEXT,
|
address TEXT,
|
||||||
zip_code VARCHAR(20),
|
zip_code VARCHAR(20),
|
||||||
city VARCHAR(100), -- Added city column
|
city VARCHAR(100),
|
||||||
country VARCHAR(100),
|
country VARCHAR(100),
|
||||||
phone_secondary VARCHAR(255),
|
phone_secondary VARCHAR(255),
|
||||||
emergency_contact_name VARCHAR(255),
|
emergency_contact_name VARCHAR(255),
|
||||||
emergency_contact_phone VARCHAR(255),
|
emergency_contact_phone VARCHAR(255),
|
||||||
account_holder_name VARCHAR(255), -- Added column
|
account_holder_name VARCHAR(255),
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
UNIQUE KEY unique_user_profile (user_id)
|
UNIQUE KEY unique_user_profile (user_id)
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
console.log('✅ Personal profiles table updated');
|
console.log('✅ Personal profiles table created/verified');
|
||||||
|
|
||||||
// 3. company_profiles table: Details specific to company users
|
// 3. company_profiles table: Details specific to company users
|
||||||
await connection.query(`
|
await connection.query(`
|
||||||
@ -146,18 +146,18 @@ async function createDatabase() {
|
|||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
user_id INT NOT NULL,
|
user_id INT NOT NULL,
|
||||||
company_name VARCHAR(255) NOT NULL,
|
company_name VARCHAR(255) NOT NULL,
|
||||||
registration_number VARCHAR(255) UNIQUE, -- allow NULL
|
registration_number VARCHAR(255) UNIQUE,
|
||||||
phone VARCHAR(255) NULL,
|
phone VARCHAR(255) NULL,
|
||||||
address TEXT,
|
address TEXT,
|
||||||
zip_code VARCHAR(20),
|
zip_code VARCHAR(20),
|
||||||
city VARCHAR(100),
|
city VARCHAR(100),
|
||||||
country VARCHAR(100), -- Added country column after city
|
country VARCHAR(100),
|
||||||
branch VARCHAR(255),
|
branch VARCHAR(255),
|
||||||
number_of_employees INT,
|
number_of_employees INT,
|
||||||
business_type VARCHAR(255),
|
business_type VARCHAR(255),
|
||||||
contact_person_name VARCHAR(255),
|
contact_person_name VARCHAR(255),
|
||||||
contact_person_phone VARCHAR(255),
|
contact_person_phone VARCHAR(255),
|
||||||
account_holder_name VARCHAR(255), -- Added column
|
account_holder_name VARCHAR(255),
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
@ -165,24 +165,7 @@ async function createDatabase() {
|
|||||||
UNIQUE KEY unique_registration_number (registration_number)
|
UNIQUE KEY unique_registration_number (registration_number)
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
console.log('✅ Company profiles table updated');
|
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(`
|
||||||
@ -310,37 +293,15 @@ async function createDatabase() {
|
|||||||
storageKey VARCHAR(255) NOT NULL,
|
storageKey VARCHAR(255) NOT NULL,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
lang VARCHAR(10) NOT NULL,
|
lang VARCHAR(10) NOT NULL,
|
||||||
user_type ENUM('personal','company','both') DEFAULT 'both', -- NEW COLUMN
|
user_type ENUM('personal','company','both') DEFAULT 'both',
|
||||||
version INT DEFAULT 1,
|
version INT DEFAULT 1,
|
||||||
state ENUM('active','inactive') DEFAULT 'inactive', -- Added state column
|
state ENUM('active','inactive') DEFAULT 'inactive',
|
||||||
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
console.log('✅ Document templates table created/verified');
|
console.log('✅ Document templates table created/verified');
|
||||||
|
|
||||||
// Ensure version column exists if table already existed
|
|
||||||
try {
|
|
||||||
await connection.query(`ALTER TABLE document_templates ADD COLUMN version INT DEFAULT 1`);
|
|
||||||
console.log('🔧 Ensured version column exists');
|
|
||||||
} catch (e) {
|
|
||||||
console.log('ℹ️ Version column already exists or ALTER not required');
|
|
||||||
}
|
|
||||||
// Ensure state column exists if table already existed
|
|
||||||
try {
|
|
||||||
await connection.query(`ALTER TABLE document_templates ADD COLUMN state ENUM('active','inactive') DEFAULT 'inactive'`);
|
|
||||||
console.log('🔧 Ensured state column exists');
|
|
||||||
} catch (e) {
|
|
||||||
console.log('ℹ️ State column already exists or ALTER not required');
|
|
||||||
}
|
|
||||||
// Ensure user_type column exists
|
|
||||||
try {
|
|
||||||
await connection.query(`ALTER TABLE document_templates ADD COLUMN user_type ENUM('personal','company','both') DEFAULT 'both'`);
|
|
||||||
console.log('🔧 Ensured user_type column exists');
|
|
||||||
} catch (e) {
|
|
||||||
console.log('ℹ️ user_type column already exists or ALTER not required');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8b. user_id_documents table: Stores ID-specific metadata (front/back object storage IDs)
|
// 8b. user_id_documents table: Stores ID-specific metadata (front/back object storage IDs)
|
||||||
await connection.query(`
|
await connection.query(`
|
||||||
CREATE TABLE IF NOT EXISTS user_id_documents (
|
CREATE TABLE IF NOT EXISTS user_id_documents (
|
||||||
@ -349,8 +310,8 @@ async function createDatabase() {
|
|||||||
document_type ENUM('personal_id', 'company_id') NOT NULL,
|
document_type ENUM('personal_id', 'company_id') NOT NULL,
|
||||||
front_object_storage_id VARCHAR(255) NOT NULL,
|
front_object_storage_id VARCHAR(255) NOT NULL,
|
||||||
back_object_storage_id VARCHAR(255) NULL,
|
back_object_storage_id VARCHAR(255) NULL,
|
||||||
original_filename_front VARCHAR(255), -- NEW COLUMN
|
original_filename_front VARCHAR(255),
|
||||||
original_filename_back VARCHAR(255), -- NEW COLUMN
|
original_filename_back VARCHAR(255),
|
||||||
id_type VARCHAR(50),
|
id_type VARCHAR(50),
|
||||||
id_number VARCHAR(100),
|
id_number VARCHAR(100),
|
||||||
expiry_date DATE,
|
expiry_date DATE,
|
||||||
@ -364,6 +325,7 @@ async function createDatabase() {
|
|||||||
await connection.query(`
|
await connection.query(`
|
||||||
CREATE TABLE IF NOT EXISTS user_action_logs (
|
CREATE TABLE IF NOT EXISTS user_action_logs (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT NULL,
|
||||||
affected_user_id INT NULL,
|
affected_user_id INT NULL,
|
||||||
action VARCHAR(100) NOT NULL,
|
action VARCHAR(100) NOT NULL,
|
||||||
performed_by_user_id INT NULL,
|
performed_by_user_id INT NULL,
|
||||||
@ -371,8 +333,10 @@ async function createDatabase() {
|
|||||||
ip_address VARCHAR(45),
|
ip_address VARCHAR(45),
|
||||||
user_agent TEXT,
|
user_agent TEXT,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE, -- NEW FK
|
||||||
FOREIGN KEY (affected_user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE,
|
FOREIGN KEY (affected_user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
FOREIGN KEY (performed_by_user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE,
|
FOREIGN KEY (performed_by_user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
|
INDEX idx_user_id (user_id), -- NEW index
|
||||||
INDEX idx_affected_user (affected_user_id),
|
INDEX idx_affected_user (affected_user_id),
|
||||||
INDEX idx_performed_by (performed_by_user_id),
|
INDEX idx_performed_by (performed_by_user_id),
|
||||||
INDEX idx_action (action),
|
INDEX idx_action (action),
|
||||||
@ -381,6 +345,57 @@ async function createDatabase() {
|
|||||||
`);
|
`);
|
||||||
console.log('✅ User action logs table created/verified');
|
console.log('✅ User action logs table created/verified');
|
||||||
|
|
||||||
|
// --- Add missing user_id column for existing databases + backfill ---
|
||||||
|
try {
|
||||||
|
// Add column if missing
|
||||||
|
await connection.query(`ALTER TABLE user_action_logs ADD COLUMN user_id INT NULL`);
|
||||||
|
console.log('🆕 Added user_action_logs.user_id column');
|
||||||
|
|
||||||
|
// Add FK if just added
|
||||||
|
try {
|
||||||
|
await connection.query(`
|
||||||
|
ALTER TABLE user_action_logs
|
||||||
|
ADD CONSTRAINT fk_user_action_logs_user
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
`);
|
||||||
|
console.log('🆕 Added FK fk_user_action_logs_user');
|
||||||
|
} catch (e) {
|
||||||
|
console.log('ℹ️ FK fk_user_action_logs_user already exists or cannot add:', e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add index if missing
|
||||||
|
try {
|
||||||
|
await connection.query(`CREATE INDEX idx_user_id ON user_action_logs (user_id)`);
|
||||||
|
console.log('🆕 Added index idx_user_id on user_action_logs.user_id');
|
||||||
|
} catch (e) {
|
||||||
|
console.log('ℹ️ idx_user_id already exists or cannot add:', e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backfill: prefer performed_by_user_id, else affected_user_id
|
||||||
|
try {
|
||||||
|
const [res1] = await connection.query(`
|
||||||
|
UPDATE user_action_logs
|
||||||
|
SET user_id = performed_by_user_id
|
||||||
|
WHERE user_id IS NULL AND performed_by_user_id IS NOT NULL
|
||||||
|
`);
|
||||||
|
const [res2] = await connection.query(`
|
||||||
|
UPDATE user_action_logs
|
||||||
|
SET user_id = affected_user_id
|
||||||
|
WHERE user_id IS NULL AND affected_user_id IS NOT NULL
|
||||||
|
`);
|
||||||
|
console.log('🧹 Backfilled user_action_logs.user_id from performed_by_user_id/affected_user_id');
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('⚠️ Could not backfill user_action_logs.user_id:', e.message);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Column may already exist; ignore
|
||||||
|
if (!/Duplicate column name|exists/i.test(e.message)) {
|
||||||
|
console.log('ℹ️ user_action_logs.user_id add skipped or not required:', e.message);
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ user_action_logs.user_id already exists');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Email & Registration Flow Tables ---
|
// --- Email & Registration Flow Tables ---
|
||||||
|
|
||||||
// 10. email_attempts table: For tracking email sending attempts
|
// 10. email_attempts table: For tracking email sending attempts
|
||||||
@ -423,43 +438,6 @@ async function createDatabase() {
|
|||||||
`);
|
`);
|
||||||
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 (
|
||||||
@ -586,14 +564,6 @@ async function createDatabase() {
|
|||||||
`);
|
`);
|
||||||
console.log('✅ Coffee table created/verified');
|
console.log('✅ Coffee table created/verified');
|
||||||
|
|
||||||
// Ensure quantity column is removed for existing databases
|
|
||||||
try {
|
|
||||||
await connection.query(`ALTER TABLE coffee_table DROP COLUMN IF EXISTS quantity`);
|
|
||||||
console.log('🔧 Removed coffee_table.quantity');
|
|
||||||
} catch (e) {
|
|
||||||
console.log('ℹ️ coffee_table.quantity already removed or ALTER not required');
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Matrix: Global 5-ary tree config and relations ---
|
// --- Matrix: Global 5-ary tree config and relations ---
|
||||||
await connection.query(`
|
await connection.query(`
|
||||||
CREATE TABLE IF NOT EXISTS matrix_config (
|
CREATE TABLE IF NOT EXISTS matrix_config (
|
||||||
@ -654,103 +624,6 @@ async function createDatabase() {
|
|||||||
`);
|
`);
|
||||||
console.log('✅ User matrix metadata table created/verified');
|
console.log('✅ User matrix metadata table created/verified');
|
||||||
|
|
||||||
// Ensure new columns exist if table already existed
|
|
||||||
try {
|
|
||||||
await connection.query(`ALTER TABLE user_matrix_metadata ADD COLUMN name VARCHAR(255) NULL`);
|
|
||||||
console.log('🔧 Ensured user_matrix_metadata.name exists');
|
|
||||||
} catch (e) {
|
|
||||||
console.log('ℹ️ user_matrix_metadata.name already exists or ALTER not required');
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await connection.query(`ALTER TABLE user_matrix_metadata ADD COLUMN is_active BOOLEAN DEFAULT TRUE`);
|
|
||||||
console.log('🔧 Ensured user_matrix_metadata.is_active exists');
|
|
||||||
} catch (e) {
|
|
||||||
console.log('ℹ️ user_matrix_metadata.is_active already exists or ALTER not required');
|
|
||||||
}
|
|
||||||
// NEW: ensure max_depth column exists
|
|
||||||
try {
|
|
||||||
await connection.query(`ALTER TABLE user_matrix_metadata ADD COLUMN max_depth INT NULL`);
|
|
||||||
console.log('🔧 Ensured user_matrix_metadata.max_depth exists');
|
|
||||||
} catch (e) {
|
|
||||||
console.log('ℹ️ user_matrix_metadata.max_depth already exists or ALTER not required');
|
|
||||||
}
|
|
||||||
|
|
||||||
// NEW: backfill max_depth policy
|
|
||||||
try {
|
|
||||||
// Master top node gets unlimited depth (NULL)
|
|
||||||
await connection.query(`
|
|
||||||
UPDATE user_matrix_metadata
|
|
||||||
SET max_depth = NULL
|
|
||||||
WHERE root_user_id IN (SELECT master_top_user_id FROM matrix_config)
|
|
||||||
`);
|
|
||||||
// All other matrices default to depth 5 where NULL
|
|
||||||
await connection.query(`
|
|
||||||
UPDATE user_matrix_metadata
|
|
||||||
SET max_depth = 5
|
|
||||||
WHERE max_depth IS NULL
|
|
||||||
AND root_user_id NOT IN (SELECT master_top_user_id FROM matrix_config)
|
|
||||||
`);
|
|
||||||
console.log('🧹 Backfilled user_matrix_metadata.max_depth (master=NULL, others=5)');
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('⚠️ Could not backfill user_matrix_metadata.max_depth:', 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'); // NEW composite index
|
|
||||||
await ensureIndex(connection, 'company_stamps', 'idx_company_stamps_company', 'company_id');
|
|
||||||
await ensureIndex(connection, 'company_stamps', 'idx_company_stamps_active', 'is_active');
|
|
||||||
|
|
||||||
// Coffee products
|
|
||||||
await ensureIndex(connection, 'coffee_table', 'idx_coffee_state', 'state');
|
|
||||||
await ensureIndex(connection, 'coffee_table', 'idx_coffee_updated_at', 'updated_at');
|
|
||||||
await ensureIndex(connection, 'coffee_table', 'idx_coffee_billing', 'billing_interval, interval_count');
|
|
||||||
await ensureIndex(connection, 'coffee_table', 'idx_coffee_sku', 'sku');
|
|
||||||
|
|
||||||
// Matrix indexes
|
|
||||||
await ensureIndex(connection, 'user_tree_edges', 'idx_user_tree_edges_parent', 'parent_user_id');
|
|
||||||
// child_user_id already has a UNIQUE constraint; extra index not needed
|
|
||||||
await ensureIndex(connection, 'user_tree_closure', 'idx_user_tree_closure_ancestor_depth', 'ancestor_user_id, depth');
|
|
||||||
await ensureIndex(connection, 'user_tree_closure', 'idx_user_tree_closure_descendant', 'descendant_user_id');
|
|
||||||
await ensureIndex(connection, 'user_matrix_metadata', 'idx_user_matrix_is_active', 'is_active'); // NEW
|
|
||||||
await ensureIndex(connection, 'user_matrix_metadata', 'idx_user_matrix_max_depth', 'max_depth'); // NEW
|
|
||||||
|
|
||||||
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();
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
|
const { logger } = require('./logger');
|
||||||
|
|
||||||
function authMiddleware(req, res, next) {
|
function authMiddleware(req, res, next) {
|
||||||
const authHeader = req.headers.authorization;
|
const authHeader = req.headers.authorization;
|
||||||
@ -9,9 +10,43 @@ function authMiddleware(req, res, next) {
|
|||||||
const token = authHeader.split(' ')[1];
|
const token = authHeader.split(' ')[1];
|
||||||
try {
|
try {
|
||||||
const payload = jwt.verify(token, process.env.JWT_SECRET);
|
const payload = jwt.verify(token, process.env.JWT_SECRET);
|
||||||
req.user = payload; // Attach user info to request
|
|
||||||
|
// edit profile context awareness
|
||||||
|
const isEditProfile =
|
||||||
|
req.originalUrl?.includes('/profile/personal') ||
|
||||||
|
req.baseUrl?.includes('/profile/personal');
|
||||||
|
|
||||||
|
// derive and log user type data
|
||||||
|
const derivedUserType = payload.userType ?? payload.user_type ?? payload.type;
|
||||||
|
const authDebug = {
|
||||||
|
context: isEditProfile ? 'edit-profile' : 'general',
|
||||||
|
method: req.method,
|
||||||
|
route: req.originalUrl,
|
||||||
|
id: payload.id ?? payload.userId ?? payload.sub,
|
||||||
|
email: payload.email,
|
||||||
|
userType: payload.userType,
|
||||||
|
user_type: payload.user_type,
|
||||||
|
derivedUserType,
|
||||||
|
payloadKeys: Object.keys(payload || {})
|
||||||
|
};
|
||||||
|
logger.info(`authMiddleware:verified context=${authDebug.context} userType=${authDebug.derivedUserType}`, authDebug);
|
||||||
|
// console fallback for local dev
|
||||||
|
console.log('[authMiddleware] verified', authDebug);
|
||||||
|
|
||||||
|
// Attach user info to request (with normalized userType for downstream checks)
|
||||||
|
req.user = {
|
||||||
|
...payload,
|
||||||
|
userType: payload.userType ?? payload.user_type,
|
||||||
|
user_type: payload.user_type ?? payload.userType
|
||||||
|
};
|
||||||
|
|
||||||
next();
|
next();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
logger.warn('authMiddleware:tokenInvalid', {
|
||||||
|
method: req.method,
|
||||||
|
route: req.originalUrl,
|
||||||
|
reason: error?.message
|
||||||
|
});
|
||||||
return res.status(401).json({ success: false, message: 'Invalid or expired access token' });
|
return res.status(401).json({ success: false, message: 'Invalid or expired access token' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -185,6 +185,196 @@ class PersonalUserRepository {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns merged profile shape used by "GET /api/me"-like responses
|
||||||
|
async getMergedProfileById(userId) {
|
||||||
|
const conn = this.unitOfWork.connection;
|
||||||
|
const [rows] = await conn.query(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
u.id, u.email, u.user_type, u.role, u.iban, u.created_at, u.updated_at,
|
||||||
|
pp.first_name, pp.last_name, pp.phone, pp.address, pp.date_of_birth, pp.account_holder_name
|
||||||
|
FROM users u
|
||||||
|
LEFT JOIN personal_profiles pp ON u.id = pp.user_id
|
||||||
|
WHERE u.id = ?
|
||||||
|
`,
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
if (!rows.length) return null;
|
||||||
|
const r = rows[0];
|
||||||
|
return {
|
||||||
|
id: r.id,
|
||||||
|
email: r.email,
|
||||||
|
user_type: r.user_type,
|
||||||
|
role: r.role,
|
||||||
|
firstName: r.first_name,
|
||||||
|
lastName: r.last_name,
|
||||||
|
phone: r.phone,
|
||||||
|
address: r.address,
|
||||||
|
dateOfBirth: r.date_of_birth,
|
||||||
|
accountHolderName: r.account_holder_name,
|
||||||
|
iban: r.iban,
|
||||||
|
createdAt: r.created_at,
|
||||||
|
updatedAt: r.updated_at
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateBasicInfo(userId, payload) {
|
||||||
|
logger.info('PersonalUserRepository.updateBasicInfo:start', { userId });
|
||||||
|
const conn = this.unitOfWork.connection;
|
||||||
|
|
||||||
|
// Lock current rows to compute diffs safely
|
||||||
|
const [[currentUser]] = await conn.query(
|
||||||
|
`SELECT id, email FROM users WHERE id = ? FOR UPDATE`,
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
const [[currentProfile]] = await conn.query(
|
||||||
|
`SELECT first_name, last_name, phone, address FROM personal_profiles WHERE user_id = ? FOR UPDATE`,
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
if (!currentUser) {
|
||||||
|
logger.warn('PersonalUserRepository.updateBasicInfo:not_found', { userId });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { firstName, lastName, email, phone, address } = payload;
|
||||||
|
|
||||||
|
// Compute diffs
|
||||||
|
const changes = {};
|
||||||
|
const setProfile = {};
|
||||||
|
const setUser = {};
|
||||||
|
|
||||||
|
if (email !== undefined && email !== currentUser.email) {
|
||||||
|
// Uniqueness check
|
||||||
|
const [conflict] = await conn.query(
|
||||||
|
`SELECT id FROM users WHERE email = ? AND id <> ? LIMIT 1`,
|
||||||
|
[email, userId]
|
||||||
|
);
|
||||||
|
if (conflict.length) {
|
||||||
|
const err = new Error('Email already in use');
|
||||||
|
err.code = 'EMAIL_CONFLICT';
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
setUser.email = email;
|
||||||
|
changes.email = { from: currentUser.email, to: email };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstName !== undefined && firstName !== currentProfile?.first_name) {
|
||||||
|
setProfile.first_name = firstName;
|
||||||
|
changes.firstName = { from: currentProfile?.first_name ?? null, to: firstName };
|
||||||
|
}
|
||||||
|
if (lastName !== undefined && lastName !== currentProfile?.last_name) {
|
||||||
|
setProfile.last_name = lastName;
|
||||||
|
changes.lastName = { from: currentProfile?.last_name ?? null, to: lastName };
|
||||||
|
}
|
||||||
|
if (phone !== undefined && phone !== currentProfile?.phone) {
|
||||||
|
setProfile.phone = phone;
|
||||||
|
changes.phone = { from: currentProfile?.phone ?? null, to: phone };
|
||||||
|
}
|
||||||
|
if (address !== undefined && address !== currentProfile?.address) {
|
||||||
|
setProfile.address = address;
|
||||||
|
changes.address = { from: currentProfile?.address ?? null, to: address };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist updates
|
||||||
|
if (Object.keys(setUser).length) {
|
||||||
|
await conn.query(
|
||||||
|
`UPDATE users SET ${Object.keys(setUser).map(k => `${k} = ?`).join(', ')} WHERE id = ?`,
|
||||||
|
[...Object.values(setUser), userId]
|
||||||
|
);
|
||||||
|
// Email changed: reset verification flags
|
||||||
|
if (setUser.email) {
|
||||||
|
await conn.query(
|
||||||
|
`UPDATE user_status SET email_verified = 0, email_verified_at = NULL WHERE user_id = ?`,
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Object.keys(setProfile).length) {
|
||||||
|
await conn.query(
|
||||||
|
`UPDATE personal_profiles SET ${Object.keys(setProfile).map(k => `${k} = ?`).join(', ')} WHERE user_id = ?`,
|
||||||
|
[...Object.values(setProfile), userId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audit log
|
||||||
|
if (Object.keys(changes).length) {
|
||||||
|
await conn.query(
|
||||||
|
`INSERT INTO user_action_logs (user_id, action, details) VALUES (?, 'profile_update_basic', ?)`,
|
||||||
|
[userId, JSON.stringify({ changes })]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('PersonalUserRepository.updateBasicInfo:success', { userId, changedKeys: Object.keys(changes) });
|
||||||
|
return this.getMergedProfileById(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateBankInfo(userId, payload) {
|
||||||
|
logger.info('PersonalUserRepository.updateBankInfo:start', { userId, payloadPreview: { ...payload, ...(payload?.iban ? { iban: `****${String(payload.iban).slice(-4)}` } : {}) } });
|
||||||
|
const conn = this.unitOfWork.connection;
|
||||||
|
|
||||||
|
// Lock current rows to compute diffs safely
|
||||||
|
const [[currentUser]] = await conn.query(
|
||||||
|
`SELECT id, iban FROM users WHERE id = ? FOR UPDATE`,
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
const [[currentProfile]] = await conn.query(
|
||||||
|
`SELECT account_holder_name FROM personal_profiles WHERE user_id = ? FOR UPDATE`,
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
if (!currentUser) {
|
||||||
|
logger.warn('PersonalUserRepository.updateBankInfo:not_found', { userId });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { accountHolderName, iban } = payload;
|
||||||
|
|
||||||
|
const changes = {};
|
||||||
|
const setUser = {};
|
||||||
|
const setProfile = {};
|
||||||
|
|
||||||
|
if (iban !== undefined && iban !== currentUser.iban) {
|
||||||
|
setUser.iban = iban;
|
||||||
|
changes.iban = { from: currentUser.iban ? `****${String(currentUser.iban).slice(-4)}` : null, to: `****${String(iban).slice(-4)}` };
|
||||||
|
}
|
||||||
|
if (accountHolderName !== undefined && accountHolderName !== currentProfile?.account_holder_name) {
|
||||||
|
setProfile.account_holder_name = accountHolderName;
|
||||||
|
changes.accountHolderName = {
|
||||||
|
from: currentProfile?.account_holder_name ?? null,
|
||||||
|
to: accountHolderName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('PersonalUserRepository.updateBankInfo:diff', {
|
||||||
|
userId,
|
||||||
|
willUpdateUsers: Object.keys(setUser),
|
||||||
|
willUpdateProfile: Object.keys(setProfile),
|
||||||
|
changes
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(setUser).length) {
|
||||||
|
await conn.query(
|
||||||
|
`UPDATE users SET ${Object.keys(setUser).map(k => `${k} = ?`).join(', ')} WHERE id = ?`,
|
||||||
|
[...Object.values(setUser), userId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (Object.keys(setProfile).length) {
|
||||||
|
await conn.query(
|
||||||
|
`UPDATE personal_profiles SET ${Object.keys(setProfile).map(k => `${k} = ?`).join(', ')} WHERE user_id = ?`,
|
||||||
|
[...Object.values(setProfile), userId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(changes).length) {
|
||||||
|
await conn.query(
|
||||||
|
`INSERT INTO user_action_logs (user_id, action, details) VALUES (?, 'profile_update_bank', ?)`,
|
||||||
|
[userId, JSON.stringify({ changes })]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('PersonalUserRepository.updateBankInfo:success', { userId, changedKeys: Object.keys(changes) });
|
||||||
|
return this.getMergedProfileById(userId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = PersonalUserRepository;
|
module.exports = PersonalUserRepository;
|
||||||
@ -6,6 +6,7 @@ const DocumentTemplateController = require('../controller/documentTemplate/Docum
|
|||||||
const CompanyStampController = require('../controller/companyStamp/CompanyStampController'); // <-- added
|
const CompanyStampController = require('../controller/companyStamp/CompanyStampController'); // <-- added
|
||||||
const CoffeeController = require('../controller/admin/CoffeeController');
|
const CoffeeController = require('../controller/admin/CoffeeController');
|
||||||
const AdminUserController = require('../controller/admin/AdminUserController');
|
const AdminUserController = require('../controller/admin/AdminUserController');
|
||||||
|
const PersonalProfileController = require('../controller/profile/PersonalProfileController'); // <-- add
|
||||||
|
|
||||||
// Helper middlewares for company-stamp
|
// Helper middlewares for company-stamp
|
||||||
function adminOnly(req, res, next) {
|
function adminOnly(req, res, next) {
|
||||||
@ -36,6 +37,10 @@ router.patch('/admin/update-user-status/:id', authMiddleware, adminOnly, AdminUs
|
|||||||
// Admin: set state for coffee product
|
// Admin: set state for coffee product
|
||||||
router.patch('/admin/coffee/:id/state', authMiddleware, adminOnly, CoffeeController.setState);
|
router.patch('/admin/coffee/:id/state', authMiddleware, adminOnly, CoffeeController.setState);
|
||||||
|
|
||||||
|
// Personal profile (self-service) - no admin guard
|
||||||
|
router.patch('/profile/personal/basic', authMiddleware, PersonalProfileController.updateBasic);
|
||||||
|
router.patch('/profile/personal/bank', authMiddleware, PersonalProfileController.updateBank);
|
||||||
|
|
||||||
// Add other PATCH routes here as needed
|
// Add other PATCH routes here as needed
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -20,6 +20,26 @@ class PersonalProfileService {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async updateBasic(userId, payload, unitOfWork) {
|
||||||
|
logger.info('PersonalProfileService.updateBasic:start', { userId });
|
||||||
|
const repo = new PersonalUserRepository(unitOfWork);
|
||||||
|
const updated = await repo.updateBasicInfo(userId, payload);
|
||||||
|
const UserStatusService = require('../../status/UserStatusService');
|
||||||
|
await UserStatusService.checkAndSetPendingIfComplete(userId, unitOfWork);
|
||||||
|
logger.info('PersonalProfileService.updateBasic:success', { userId });
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async updateBank(userId, payload, unitOfWork) {
|
||||||
|
logger.info('PersonalProfileService.updateBank:start', { userId });
|
||||||
|
const repo = new PersonalUserRepository(unitOfWork);
|
||||||
|
const updated = await repo.updateBankInfo(userId, payload);
|
||||||
|
const UserStatusService = require('../../status/UserStatusService');
|
||||||
|
await UserStatusService.checkAndSetPendingIfComplete(userId, unitOfWork);
|
||||||
|
logger.info('PersonalProfileService.updateBank:success', { userId });
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = PersonalProfileService;
|
module.exports = PersonalProfileService;
|
||||||
Loading…
Reference in New Issue
Block a user