244 lines
10 KiB
JavaScript
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;
|