CentralBackend/controller/profile/PersonalProfileController.js
2025-11-18 01:21:09 +01:00

244 lines
10 KiB
JavaScript

const UnitOfWork = require('../../database/UnitOfWork');
const PersonalProfileService = require('../../services/profile/personal/PersonalProfileService');
const PersonalUserRepository = require('../../repositories/user/personal/PersonalUserRepository');
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 {
static async completeProfile(req, res) {
const userId = req.user.userId;
const profileData = req.body;
const unitOfWork = new UnitOfWork();
await unitOfWork.start();
logger.info('PersonalProfileController:completeProfile:start', { userId, profileData });
try {
const repo = new PersonalUserRepository(unitOfWork);
await repo.updateProfileAndMarkCompleted(userId, profileData); // Correct method name
await unitOfWork.commit();
logger.info('PersonalProfileController:completeProfile:success', { userId });
res.json({ success: true, message: 'Personal profile completed successfully' });
} catch (error) {
await unitOfWork.rollback(error);
logger.error('PersonalProfileController:completeProfile:error', { userId, error });
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;