add: more things

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-07-04 03:35:09 +08:00
parent a572c1e25f
commit 3c13bb9821
36 changed files with 896 additions and 928 deletions
+124
View File
@@ -16,6 +16,9 @@ const mdl_UserSessions = require('../../models/users/user_sessions.mdl');
const mdl_Achievements = require('../../models/users/achievements.mdl');
const mdl_UserBans = require('../../models/users/user_bans.mdl');
const { mdl_UserGroups, mdl_UserGroupMembers } = require('../../models/users/user_groups.mdl');
const mdl_UserTiers = require('../../models/tiers/user_tiers.mdl');
const mdl_Payments = require('../../models/tiers/payments.mdl');
const mdl_QuizAttempt = require('../../models/courses/quiz_attempt.mdl');
const { sendEmail } = require('../../services/email.service');
const { fmtDate } = require('../../utils/datetime.util');
@@ -291,6 +294,127 @@ exports.bulkRestoreUsers = async (req, res) => {
}
};
// ─── PERMANENT DELETE ─────────────────────────────────────────────────────────
//
// The live DB has several FKs on users.user_id that are NO ACTION rather than
// the CASCADE the migration source claims (schema drift) — achievements,
// quiz_attempts, user_tiers, and payments all block a hard delete unless their
// rows for this user are removed first. Also null out the "who did this"
// actor columns (granted_by/revoked_by/lifted_by) on OTHER users' records,
// since those aren't this user's own data and shouldn't be deleted.
//
// UserBan.banned_by is NOT NULL (a ban record must always keep its banner),
// so a user who has ever banned someone can't be nulled out or hard-deleted —
// getBannerBlockedIds() below identifies those and callers must exclude them.
async function purgeUserDependents(userIds, t) {
await mdl_Achievements.destroy({ where: { user_id: userIds }, transaction: t });
await mdl_QuizAttempt.destroy({ where: { user_id: userIds }, transaction: t });
await mdl_UserTiers.destroy({ where: { user_id: userIds }, transaction: t });
await mdl_Payments.destroy({ where: { user_id: userIds }, transaction: t });
await mdl_Achievements.update({ granted_by: null }, { where: { granted_by: userIds }, transaction: t });
await mdl_UserTiers.update({ granted_by: null }, { where: { granted_by: userIds }, transaction: t });
await mdl_UserTiers.update({ revoked_by: null }, { where: { revoked_by: userIds }, transaction: t });
await mdl_UserBans.update({ lifted_by: null }, { where: { lifted_by: userIds }, transaction: t });
}
async function getBannerBlockedIds(userIds, t) {
const bans = await mdl_UserBans.findAll({
where: { banned_by: userIds }, attributes: ['banned_by'], group: ['banned_by'], transaction: t,
});
return bans.map((b) => b.banned_by);
}
exports.permanentlyDeleteUser = async (req, res) => {
const t = await sequelize.transaction();
try {
if (Number(req.params.id) === req.user.user_id) {
await t.rollback();
return R.error(res, 'You cannot permanently delete your own account.', 400);
}
const user = await mdl_Users.findOne({
where: { user_id: req.params.id }, paranoid: false, transaction: t,
});
if (!user) { await t.rollback(); return R.error(res, 'User not found.', 404); }
if (!user.deletedAt) { await t.rollback(); return R.error(res, 'User must be deactivated before it can be permanently deleted.', 400); }
const bannerBlockedIds = await getBannerBlockedIds([user.user_id], t);
if (bannerBlockedIds.length) {
await t.rollback();
return R.error(res, 'Cannot delete: this user has banned other users, and ban records must keep their banner on file.', 400);
}
await purgeUserDependents([user.user_id], t);
await user.destroy({ force: true, transaction: t });
await t.commit();
logActivity(req.user.user_id, 'permanently_delete_user', { entityType: 'user', entityId: Number(req.params.id) });
return R.success(res, 'User permanently deleted.');
} catch (err) {
await t.rollback();
console.error('[ADMIN][PERMANENT DELETE USER]', err);
return R.error(res, 'Could not permanently delete user.', 500);
}
};
// ─── BULK PERMANENT DELETE ────────────────────────────────────────────────────
exports.bulkPermanentlyDeleteUsers = async (req, res) => {
const t = await sequelize.transaction();
try {
const { ids } = req.body;
if (!Array.isArray(ids) || !ids.length) {
await t.rollback();
return R.error(res, 'No user IDs provided.', 400);
}
if (ids.includes(req.user.user_id)) {
await t.rollback();
return R.error(res, 'You cannot permanently delete your own account.', 400);
}
const users = await mdl_Users.findAll({ where: { user_id: ids }, paranoid: false, transaction: t });
if (!users.length) { await t.rollback(); return R.error(res, 'No users found.', 404); }
const deletedUsers = users.filter((u) => u.deletedAt);
if (!deletedUsers.length) {
await t.rollback();
return R.error(res, 'All selected users must be deactivated before they can be permanently deleted.', 400);
}
const archivedIds = deletedUsers.map((u) => u.user_id);
const bannerBlockedIds = await getBannerBlockedIds(archivedIds, t);
const deletedIds = archivedIds.filter((id) => !bannerBlockedIds.includes(id));
if (!deletedIds.length) {
await t.rollback();
return R.error(res, 'Cannot delete: all selected users have banned other users, and ban records must keep their banner on file.', 400);
}
await purgeUserDependents(deletedIds, t);
await mdl_Users.destroy({ where: { user_id: deletedIds }, force: true, transaction: t });
await t.commit();
logActivity(req.user.user_id, 'bulk_permanently_delete_users', {
entityType: 'user',
details: { ids: deletedIds, count: deletedIds.length },
});
return R.success(res, `${deletedIds.length} user(s) permanently deleted.`, {
deleted_ids: deletedIds,
skipped_ids: ids.filter((id) => !deletedIds.includes(id)),
});
} catch (err) {
await t.rollback();
console.error('[ADMIN][BULK PERMANENT DELETE USERS]', err);
return R.error(res, 'Could not permanently delete users.', 500);
}
};
// ─── SESSIONS ─────────────────────────────────────────────────────────────────
exports.getUserSessions = async (req, res) => {