mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
chore: relocate backend into apps/api ahead of monorepo merge
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* 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 trustedDevice = require('../../services/trustedDevice.service');
|
||||
const logActivity = require('../../utils/logActivity.util');
|
||||
const R = require('../../utils/response.util');
|
||||
const { replaceUserAvatar, removeUserAvatar } = require('../../services/avatar.service');
|
||||
const { resolveUserAvatar } = require('../../utils/resolveAvatar.util');
|
||||
|
||||
// ─── 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.', await resolveUserAvatar(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.', await resolveUserAvatar(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 },
|
||||
});
|
||||
await trustedDevice.revokeBySessionId(session.session_id);
|
||||
|
||||
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);
|
||||
|
||||
const avatarMeta = await replaceUserAvatar(user, req.file);
|
||||
|
||||
const merged = { ...(user.personal_info || {}), avatar: avatarMeta };
|
||||
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.', await resolveUserAvatar(updated));
|
||||
} catch (err) {
|
||||
if (err.status === 400) return R.error(res, err.message, 400);
|
||||
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);
|
||||
|
||||
await removeUserAvatar(user);
|
||||
|
||||
const merged = { ...(user.personal_info || {}), avatar: null };
|
||||
await user.update({ personal_info: merged });
|
||||
|
||||
return R.success(res, 'Avatar removed.');
|
||||
} catch (err) {
|
||||
if (err.status === 404) return R.error(res, err.message, 404);
|
||||
console.error('[CLIENT] deleteAvatar error:', err);
|
||||
return R.error(res, 'Could not remove avatar.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── DELETE own account ────────────────────────────────────────────────────────
|
||||
|
||||
exports.deleteAccount = async (req, res) => {
|
||||
try {
|
||||
const user = await mdl_Users.findByPk(req.user.user_id);
|
||||
if (!user) return R.error(res, 'User not found.', 404);
|
||||
|
||||
// Revoke all active sessions first
|
||||
await mdl_UserSessions.update(
|
||||
{ is_active: false, logout_info: { date: new Date().toISOString(), ip_address: req.ip, reason: 'account_deleted' } },
|
||||
{ where: { user_id: req.user.user_id, is_active: true } },
|
||||
);
|
||||
await trustedDevice.revokeAllForUser(req.user.user_id);
|
||||
|
||||
// Anonymize email before soft-delete so the unique slot is freed for re-registration
|
||||
await user.update({ email: `deleted_${req.user.user_id}@deleted.invalid`, deletedBy: req.user.user_id });
|
||||
await user.destroy(); // paranoid soft-delete — sets deleted_at
|
||||
|
||||
logActivity(req.user.user_id, 'delete_account');
|
||||
|
||||
res.clearCookie('refreshToken');
|
||||
res.clearCookie('_csrf');
|
||||
|
||||
return R.success(res, 'Account deleted.');
|
||||
} catch (err) {
|
||||
console.error('[CLIENT] deleteAccount error:', err);
|
||||
return R.error(res, 'Could not delete account.', 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);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user