/*********************************************************************************************************************************************************************** * File Name: profile.controller.js (client) * Type of Program: Controller * Description: Self-service profile management for all end users. * All routes require: authenticate → requireClient() * * Endpoints: * GET /api/client/profile → view own profile * PUT /api/client/profile → update personal_info * GET /api/client/sessions → view own active sessions * DELETE /api/client/sessions/:id → revoke a specific session * GET /api/client/achievements → view own achievements * * Author: rgrgogu * Date Created: Oct. 6, 2025 ***********************************************************************************************************************************************************************/ const mdl_Users = require('../../models/users/users.mdl'); const mdl_UserSessions = require('../../models/users/user_sessions.mdl'); const mdl_Achievements = require('../../models/users/achievements.mdl'); const logActivity = require('../../utils/logActivity.util'); const R = require('../../utils/response.util'); const { uploadFile, deleteFile } = require('../../services/s3.service'); // ─── GET own profile ─────────────────────────────────────────────────────────── exports.getProfile = async (req, res) => { try { const user = await mdl_Users.findByPk(req.user.user_id, { attributes: { exclude: ['password', 'otp_code', 'otp_expires_at'] }, }); return R.success(res, 'Profile retrieved.', user); } catch (err) { return R.error(res, 'Could not retrieve profile.', 500); } }; // ─── PUT update own profile ──────────────────────────────────────────────────── exports.updateProfile = async (req, res) => { try { const user = await mdl_Users.findByPk(req.user.user_id); const { personal_info } = req.body; // Deep-merge personal_info so partial updates don't wipe existing data const merged = { ...(user.personal_info || {}), ...(personal_info || {}), name: { ...((user.personal_info?.name) || {}), ...((personal_info?.name) || {}), }, }; await user.update({ personal_info: merged, needs_intro: false }); const updated = await mdl_Users.findByPk(req.user.user_id, { attributes: { exclude: ['password', 'otp_code', 'otp_expires_at'] }, }); logActivity(req.user.user_id, 'update_profile'); return R.success(res, 'Profile updated.', updated); } catch (err) { console.error('[CLIENT] updateProfile error:', err); return R.error(res, 'Profile update failed.', 500); } }; // ─── GET own sessions ────────────────────────────────────────────────────────── exports.getSessions = async (req, res) => { try { const sessions = await mdl_UserSessions.findAll({ where: { user_id: req.user.user_id, is_active: true }, order: [['createdAt', 'DESC']], attributes: { exclude: ['refresh_token_hash'] }, }); return R.success(res, 'Sessions retrieved.', sessions); } catch (err) { return R.error(res, 'Could not retrieve sessions.', 500); } }; // ─── DELETE revoke a session ─────────────────────────────────────────────────── exports.revokeSession = async (req, res) => { try { const session = await mdl_UserSessions.findOne({ where: { session_id: req.params.id, user_id: req.user.user_id }, }); if (!session) return R.error(res, 'Session not found.', 404); await session.update({ is_active: false, logout_info: { date: new Date().toISOString(), ip_address: req.ip }, }); logActivity(req.user.user_id, 'revoke_session', { entityType: 'session', entityId: session.session_id }); return R.success(res, 'Session revoked.'); } catch (err) { return R.error(res, 'Could not revoke session.', 500); } }; // ─── POST upload own avatar ──────────────────────────────────────────────────── exports.uploadAvatar = async (req, res) => { try { if (!req.file) return R.error(res, 'No file provided.', 400); const user = await mdl_Users.findByPk(req.user.user_id); // Remove old avatar from S3 before replacing const oldKey = user.personal_info?.avatar?.uuid; if (oldKey) await deleteFile(oldKey).catch(() => {}); const { url, uuid } = await uploadFile({ buffer: req.file.buffer, originalname: req.file.originalname, mimetype: req.file.mimetype, ownerType: 'avatar', }); const merged = { ...(user.personal_info || {}), avatar: { url, uuid, name: req.file.originalname, mime_type: req.file.mimetype, size: req.file.size, }, }; await user.update({ personal_info: merged }); const updated = await mdl_Users.findByPk(req.user.user_id, { attributes: { exclude: ['password', 'otp_code', 'otp_expires_at'] }, }); return R.success(res, 'Avatar updated.', updated); } catch (err) { console.error('[CLIENT] uploadAvatar error:', err); return R.error(res, 'Avatar upload failed.', 500); } }; // ─── DELETE remove own avatar ────────────────────────────────────────────────── exports.deleteAvatar = async (req, res) => { try { const user = await mdl_Users.findByPk(req.user.user_id); const key = user.personal_info?.avatar?.uuid; if (!key) return R.error(res, 'No avatar to remove.', 404); await deleteFile(key).catch(() => {}); const merged = { ...(user.personal_info || {}), avatar: null }; await user.update({ personal_info: merged }); return R.success(res, 'Avatar removed.'); } catch (err) { console.error('[CLIENT] deleteAvatar error:', err); return R.error(res, 'Could not remove avatar.', 500); } }; // ─── GET own achievements ────────────────────────────────────────────────────── exports.getAchievements = async (req, res) => { try { const achievements = await mdl_Achievements.findAll({ where: { user_id: req.user.user_id }, order: [['granted_at', 'DESC']], }); return R.success(res, 'Achievements retrieved.', achievements); } catch (err) { return R.error(res, 'Could not retrieve achievements.', 500); } };