Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-08-13 19:58:52 +08:00
parent 9018d6d158
commit 2e9c2ad43f
23 changed files with 954 additions and 287 deletions
@@ -137,9 +137,9 @@ exports.getCourseReadingProgress = async (req, res) => {
return {
...entry,
user: {
email: u?.email ?? null,
full_name: u?.personal_info?.name?.full_name ?? null,
avatar_url: avatar?.url ?? null,
email: u?.email ?? null,
full_name: u?.personal_info?.name?.full_name ?? null,
avatar_stream_token: avatar?.stream_token ?? null,
},
units_total,
lessons_total,
+9 -9
View File
@@ -2614,13 +2614,13 @@ exports.syncInstructors = async (req, res) => {
// ─── COMPLETIONS HELPERS ──────────────────────────────────────────────────────
async function extractUserInfo(user) {
if (!user) return { full_name: null, email: null, avatar_url: null, deleted: false };
if (!user) return { full_name: null, email: null, avatar_stream_token: null, deleted: false };
const avatar = await resolveAvatarUrl(user.personal_info?.avatar);
return {
full_name: user.personal_info?.name?.full_name ?? null,
email: user.email ?? null,
avatar_url: avatar?.url ?? null,
deleted: !!user.deletedAt,
full_name: user.personal_info?.name?.full_name ?? null,
email: user.email ?? null,
avatar_stream_token: avatar?.stream_token ?? null,
deleted: !!user.deletedAt,
};
}
@@ -2629,12 +2629,12 @@ async function groupByUser(attempts) {
for (const a of attempts) {
const uid = String(a.user_id);
if (!map.has(uid)) {
const { full_name, email, avatar_url, deleted } = await extractUserInfo(a.user);
const { full_name, email, avatar_stream_token, deleted } = await extractUserInfo(a.user);
map.set(uid, {
user_id: a.user_id,
full_name,
email,
avatar_url,
avatar_stream_token,
deleted,
attempt_count: 0,
best_score: 0,
@@ -2757,7 +2757,7 @@ exports.getAssessmentSessions = async (req, res) => {
const rows = await Promise.all(sessions.map(async (s) => {
const j = s.toJSON();
const { full_name, email, avatar_url, deleted } = await extractUserInfo(j.user);
const { full_name, email, avatar_stream_token, deleted } = await extractUserInfo(j.user);
const time_spent_seconds = j.status !== 'in_progress' && j.started_at
? Math.round((new Date(j.updatedAt) - new Date(j.started_at)) / 1000)
: null;
@@ -2766,7 +2766,7 @@ exports.getAssessmentSessions = async (req, res) => {
user_id: j.user_id,
full_name,
email,
avatar_url,
avatar_stream_token,
deleted,
status: j.status,
started_at: j.started_at,
-10
View File
@@ -79,21 +79,11 @@ function makeProductHandlers(purchasable_type, paramName) {
}
const courseProductHandlers = makeProductHandlers('course', 'courseId');
const unitProductHandlers = makeProductHandlers('unit', 'unitId');
const lessonProductHandlers = makeProductHandlers('lesson', 'lessonId');
exports.getCourseProduct = courseProductHandlers.get;
exports.upsertCourseProduct = courseProductHandlers.upsert;
exports.removeCourseProduct = courseProductHandlers.remove;
exports.getUnitProduct = unitProductHandlers.get;
exports.upsertUnitProduct = unitProductHandlers.upsert;
exports.removeUnitProduct = unitProductHandlers.remove;
exports.getLessonProduct = lessonProductHandlers.get;
exports.upsertLessonProduct = lessonProductHandlers.upsert;
exports.removeLessonProduct = lessonProductHandlers.remove;
// ─── CATEGORIES (per course) ──────────────────────────────────────────────────
exports.getCourseCategories = async (req, res) => {
+9 -28
View File
@@ -15,10 +15,10 @@
***********************************************************************************************************************************************************************/
'use strict';
const mdl_Users = require('../../models/users/users.mdl');
const R = require('../../utils/response.util');
const { uploadFile, deleteFile } = require('../../services/s3.service');
const { resolveUserAvatar } = require('../../utils/resolveAvatar.util');
const mdl_Users = require('../../models/users/users.mdl');
const R = require('../../utils/response.util');
const { replaceUserAvatar, removeUserAvatar } = require('../../services/avatar.service');
const { resolveUserAvatar } = require('../../utils/resolveAvatar.util');
// ─── GET own profile ───────────────────────────────────────────────────────────
@@ -69,28 +69,9 @@ exports.uploadAvatar = async (req, res) => {
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,
},
};
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, {
@@ -98,6 +79,7 @@ exports.uploadAvatar = async (req, res) => {
});
return R.success(res, 'Avatar updated.', await resolveUserAvatar(updated));
} catch (err) {
if (err.status === 400) return R.error(res, err.message, 400);
console.error('[ADMIN] uploadAvatar error:', err);
return R.error(res, 'Avatar upload failed.', 500);
}
@@ -108,16 +90,15 @@ exports.uploadAvatar = async (req, res) => {
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(() => {});
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('[ADMIN] deleteAvatar error:', err);
return R.error(res, 'Could not remove avatar.', 500);
}
+12 -17
View File
@@ -29,7 +29,7 @@ const { paginate } = require('../../utils/paginate.util');
const { getFieldValues } = require('../../utils/fieldValues.util');
const logActivity = require('../../utils/logActivity.util');
const { snapshotPlanGrants } = require('../../services/tierGrants.service');
const { revokePlanSubscriberAccess } = require('../../services/planAccess.service');
const { revokePlanSubscriberAccess, revokePlanSubscriberAccessBulk } = require('../../services/planAccess.service');
const {
excludeAttributes: plansExclude,
@@ -244,16 +244,14 @@ exports.bulkArchivePlans = async (req, res) => {
await mdl_TierPlans.destroy({ where: { plan_id: activeIds } });
// Archiving always force-revokes current subscribers' access (no refund) —
// each plan fires its own tier_plan_access_revoked (needs each plan's own
// label), not the old batched "access unaffected" tier_plan_archived notice.
// one batched call across all selected plans (each still fires its own
// tier_plan_access_revoked with its own label) instead of one revoke call
// per plan, so this stays O(1) DB round trips regardless of selection size.
let revoked_user_count = 0;
for (const p of activePlans) {
try {
const { revoked_user_count: c } = await revokePlanSubscriberAccess(p, req.user?.user_id ?? null);
revoked_user_count += c;
} catch (revokeErr) {
console.error('[ADMIN][BULK ARCHIVE PLANS][REVOKE ACCESS]', revokeErr);
}
try {
({ revoked_user_count } = await revokePlanSubscriberAccessBulk(activePlans, req.user?.user_id ?? null));
} catch (revokeErr) {
console.error('[ADMIN][BULK ARCHIVE PLANS][REVOKE ACCESS]', revokeErr);
}
logActivity(req.user?.user_id, 'bulk_archive_tier_plans', { entityType: 'tier_plan', details: { ids: activeIds, count: activeIds.length, revoked_user_count } });
@@ -380,13 +378,10 @@ exports.bulkPermanentlyDeletePlans = async (req, res) => {
const archivedIds = archivedPlans.map((p) => p.plan_id);
// Same reasoning as the single-delete path above: revoke any remaining
// active subscribers per plan (each needs its own label for the
// notification/email) before the records are gone for good.
let revoked_user_count = 0;
for (const p of archivedPlans) {
const { revoked_user_count: c } = await revokePlanSubscriberAccess(p, req.user?.user_id ?? null);
revoked_user_count += c;
}
// active subscribers (each plan still gets its own label on the
// notification/email) before the records are gone for good — batched in
// one call across all selected plans instead of one call per plan.
const { revoked_user_count } = await revokePlanSubscriberAccessBulk(archivedPlans, req.user?.user_id ?? null);
// payments.plan_id is RESTRICT at the DB level (payments are paranoid/soft-deleted
// by default) — plans can't be force-destroyed while payment rows still
@@ -123,12 +123,12 @@ async function formatRow(row) {
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_url: avatar?.url ?? null,
acc_type: r.user?.acc_type ?? null,
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,
+2 -1
View File
@@ -28,6 +28,7 @@ const R = require('../../utils/response.util');
const { paginate } = require('../../utils/paginate.util');
const { enrichPersonalInfo } = require('../../utils/personalInfo.util');
const { getFieldValues } = require("../../utils/fieldValues.util");
const { resolveUserAvatar } = require('../../utils/resolveAvatar.util');
const { excludeAttributes: usersExclude, jsonbSchemas: usersSchemas, computedAttributes: userComputed } = require('../../models/users/users.attributes');
@@ -119,7 +120,7 @@ exports.getUser = async (req, res) => {
});
if (!user) return R.error(res, 'User not found.', 404);
return R.success(res, 'User retrieved.', user);
return R.success(res, 'User retrieved.', await resolveUserAvatar(user));
} catch (err) {
console.error('[ADMIN][GET USER]', err);
return R.error(res, 'Could not retrieve user.', 500);