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
@@ -0,0 +1,138 @@
/***********************************************************************************************************************************************************************
* File Name: user_activity.controller.js (admin)
* Type of Program: Controller
* Description: Admin-only view of the user_activity log.
*
* GET /api/admin/activity → global paginated activity feed (all users)
* GET /api/admin/users/:id/activity → paginated activity for a single user
*
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jun. 21, 2026
***********************************************************************************************************************************************************************/
const { Op } = require('sequelize');
const mdl_UserActivity = require('../../models/users/user_activity.mdl');
const mdl_Users = require('../../models/users/users.mdl');
const R = require('../../utils/response.util');
const { resolveAvatarUrl } = require('../../utils/resolveAvatar.util');
const USER_ATTRS = [
'user_id', 'email', 'acc_type',
// full_name from JSONB — resolved in the association below
];
// ─── Shared query builder ─────────────────────────────────────────────────────
function buildWhere(query, extraWhere = {}) {
const where = { ...extraWhere };
if (query.action)
where.action = query.action;
if (query.from || query.to) {
where.created_at = {};
if (query.from) where.created_at[Op.gte] = new Date(query.from);
if (query.to) {
// `to` arrives as a date-only string (e.g. "2026-06-04"), which parses
// to that day's UTC midnight — an Op.lte against midnight excludes
// every event that happened later the same day. Push it to the last
// instant of that calendar day instead.
const to = new Date(query.to);
to.setUTCHours(23, 59, 59, 999);
where.created_at[Op.lte] = to;
}
}
return where;
}
// ─── GET GLOBAL ACTIVITY FEED ─────────────────────────────────────────────────
exports.getActivity = async (req, res) => {
try {
const page = Math.max(1, parseInt(req.query.page) || 1);
const limit = Math.min(100, parseInt(req.query.limit) || 20);
const offset = (page - 1) * limit;
const where = buildWhere(req.query);
const { count, rows } = await mdl_UserActivity.findAndCountAll({
where,
include: [{
model: mdl_Users,
as: 'user',
attributes: ['user_id', 'email', 'acc_type', 'personal_info'],
}],
order: [['created_at', 'DESC']],
limit,
offset,
});
const data = await Promise.all(rows.map(formatRow));
return R.success(res, 'Activity feed retrieved.', {
total: count,
page,
totalPages: Math.ceil(count / limit),
activities: data,
});
} catch (err) {
console.error('[ADMIN][GET ACTIVITY FEED]', err);
return R.error(res, 'Could not retrieve activity feed.', 500);
}
};
// ─── GET PER-USER ACTIVITY ────────────────────────────────────────────────────
exports.getUserActivity = async (req, res) => {
try {
const { id } = req.params;
if (!id || id === 'undefined') return R.error(res, 'Invalid User ID.', 400);
const user = await mdl_Users.findByPk(id, { attributes: ['user_id'] });
if (!user) return R.error(res, 'User not found.', 404);
const page = Math.max(1, parseInt(req.query.page) || 1);
const limit = Math.min(100, parseInt(req.query.limit) || 20);
const offset = (page - 1) * limit;
const where = buildWhere(req.query, { user_id: id });
const { count, rows } = await mdl_UserActivity.findAndCountAll({
where,
order: [['created_at', 'DESC']],
limit,
offset,
});
return R.success(res, 'User activity retrieved.', {
total: count,
page,
totalPages: Math.ceil(count / limit),
activities: rows,
});
} catch (err) {
console.error('[ADMIN][GET USER ACTIVITY]', err);
return R.error(res, 'Could not retrieve user activity.', 500);
}
};
// ─── Helpers ──────────────────────────────────────────────────────────────────
async function formatRow(row) {
const r = row.toJSON();
const info = r.user?.personal_info;
const avatar = await resolveAvatarUrl(info?.avatar);
return {
activity_id: r.activity_id,
user_id: r.user_id,
email: r.user?.email ?? null,
full_name: info?.name?.full_name ?? null,
avatar_stream_token: avatar?.stream_token ?? null,
acc_type: r.user?.acc_type ?? null,
action: r.action,
entity_type: r.entity_type,
entity_id: r.entity_id,
details: r.details,
created_at: r.created_at,
};
}