chore: relocate backend into apps/api ahead of monorepo merge

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-24 12:20:40 +08:00
co-authored by Claude Sonnet 5
parent af44598df6
commit eaa6d2276a
388 changed files with 0 additions and 13998 deletions
+152
View File
@@ -0,0 +1,152 @@
/***********************************************************************************************************************************************************************
* File Name: users.ctrl.js (staff)
* Type of Program: Controller
* Description: Staff-scoped user endpoints.
* A staff member can only see users who belong to at least one
* of the same groups the staff member belongs to.
***********************************************************************************************************************************************************************/
const { Op } = require('sequelize');
const mdl_Users = require('../../models/users/users.mdl');
const { mdl_UserGroups, mdl_UserGroupMembers } = require('../../models/users/user_groups.mdl');
/**
* GET /api/staff/users
* Returns all users who share at least one group with the requesting staff member.
* Supports optional query: ?group_id=27 or ?group_code=CG2027
*/
const getUsers = async (req, res) => {
try {
const staffUserId = req.user.user_id;
const { group_id, group_code, page = 1, limit = 20 } = req.query;
const offset = (parseInt(page) - 1) * parseInt(limit);
// 1. Find the groups the staff member belongs to
const staffMemberships = await mdl_UserGroupMembers.findAll({
where: { user_id: staffUserId },
attributes: ['group_id'],
raw: true,
});
let scopedGroupIds = staffMemberships.map(m => m.group_id);
if (scopedGroupIds.length === 0) {
return res.status(200).json({ success: true, data: [], total: 0, page, limit });
}
// 2. Filter down to a specific group if requested
if (group_id) {
const gid = parseInt(group_id);
if (!scopedGroupIds.includes(gid)) {
return res.status(403).json({ success: false, message: 'You do not have access to this group.' });
}
scopedGroupIds = [gid];
}
if (group_code) {
const group = await mdl_UserGroups.findOne({
where: { group_code: group_code.toUpperCase().trim(), deletedAt: null },
attributes: ['group_id'],
raw: true,
});
if (!group || !scopedGroupIds.includes(group.group_id)) {
return res.status(403).json({ success: false, message: 'You do not have access to this group.' });
}
scopedGroupIds = [group.group_id];
}
// 3. Find all user_ids in those groups (excluding the staff member themselves)
const memberships = await mdl_UserGroupMembers.findAll({
where: { group_id: { [Op.in]: scopedGroupIds } },
attributes: ['user_id'],
raw: true,
});
const userIds = [...new Set(memberships.map(m => m.user_id))].filter(id => id !== staffUserId);
// 4. Fetch users
const { count, rows } = await mdl_Users.findAndCountAll({
where: { user_id: { [Op.in]: userIds } },
attributes: [
'user_id', 'email', 'is_active', 'is_verified',
'acc_type', 'personal_info', 'createdAt',
],
include: [
{
model: mdl_UserGroups,
as: 'groups',
attributes: ['group_id', 'name', 'group_code'],
through: { attributes: ['joined_at'] },
where: { group_id: { [Op.in]: scopedGroupIds } },
required: true,
},
],
limit: parseInt(limit),
offset,
order: [['createdAt', 'DESC']],
});
return res.status(200).json({
success: true,
data: rows,
total: count,
page: parseInt(page),
limit: parseInt(limit),
totalPages: Math.ceil(count / parseInt(limit)),
});
} catch (err) {
console.error('[staff/users] getUsers error:', err);
return res.status(500).json({ success: false, message: 'Internal server error.' });
}
};
/**
* GET /api/staff/users/:user_id
* Get a single user's profile — only if they share a group with the staff member.
*/
const getUserById = async (req, res) => {
try {
const staffUserId = req.user.user_id;
const targetUserId = parseInt(req.params.user_id);
// Get staff's groups
const staffMemberships = await mdl_UserGroupMembers.findAll({
where: { user_id: staffUserId },
attributes: ['group_id'],
raw: true,
});
const scopedGroupIds = staffMemberships.map(m => m.group_id);
// Check target user shares a group
const sharedMembership = await mdl_UserGroupMembers.findOne({
where: {
user_id: targetUserId,
group_id: { [Op.in]: scopedGroupIds },
},
});
if (!sharedMembership) {
return res.status(403).json({ success: false, message: 'User not in your scope.' });
}
const user = await mdl_Users.findByPk(targetUserId, {
attributes: ['user_id', 'email', 'is_active', 'is_verified', 'acc_type', 'personal_info', 'createdAt'],
include: [
{
model: mdl_UserGroups,
as: 'groups',
attributes: ['group_id', 'name', 'group_code'],
through: { attributes: ['joined_at'] },
},
],
});
if (!user) return res.status(404).json({ success: false, message: 'User not found.' });
return res.status(200).json({ success: true, data: user });
} catch (err) {
console.error('[staff/users] getUserById error:', err);
return res.status(500).json({ success: false, message: 'Internal server error.' });
}
};
module.exports = { getUsers, getUserById };