test again

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-07-13 12:46:38 +08:00
parent e5e2580c7d
commit f5254f7571
12 changed files with 441 additions and 461 deletions
+30 -2
View File
@@ -7,6 +7,7 @@ const Asset = require("../../models/assets/assets.mdl");
const chibi = require("../../services/chibisafe.service");
const s3 = require("../../services/s3.service");
const mediaToken = require("../../services/mediaToken.service");
const uploadProgress = require("../../services/uploadProgress.service");
const { extractVideoMeta } = require("../../services/ffprobe.service");
const R = require('../../utils/response.util');
const { paginate } = require("../../utils/paginate.util");
@@ -328,7 +329,7 @@ exports.getAsset = async (req, res) => {
// per-file thumbnail step, so videos land with thumbnail_url null and pick
// one up later via the existing "thumbnail-only" path in updateAsset().
//
async function createAssetFromUpload({ file, thumbFile, body, user, requireVideoThumbnail = true }) {
async function createAssetFromUpload({ file, thumbFile, body, user, requireVideoThumbnail = true, uploadId }) {
const uploadedFiles = []; // [{ key, provider }]
try {
@@ -367,11 +368,24 @@ async function createAssetFromUpload({ file, thumbFile, body, user, requireVideo
if (usesProvider) {
const svc = getProvider(storage_provider);
// Real Express -> Garage progress, main file only (not the thumbnail —
// it's small enough that tracking it wouldn't add anything useful).
// Relayed live to the browser over SSE; see uploadProgress.service.js.
const onProgress = (storage_provider === "s3" && uploadId)
? ({ loaded, total }) => uploadProgress.publish(uploadId, {
phase: "storing",
loaded,
total,
pct: total ? Math.round((loaded / total) * 100) : 0,
})
: undefined;
const result = await svc.uploadFile({
buffer: file.buffer,
originalname: file.originalname,
mimetype: mime_type,
ownerType: file_type,
onProgress,
});
file_url = result.url;
storage_key_resolved = result.uuid;
@@ -497,21 +511,35 @@ async function createAssetFromUpload({ file, thumbFile, body, user, requireVideo
}
exports.uploadAsset = async (req, res) => {
const { uploadId } = req.body;
try {
const file = req.files?.file?.[0];
const thumbFile = req.files?.thumbnail?.[0];
const asset = await createAssetFromUpload({ file, thumbFile, body: req.body, user: req.user });
const asset = await createAssetFromUpload({ file, thumbFile, body: req.body, user: req.user, uploadId });
invalidateListCache();
if (uploadId) uploadProgress.complete(uploadId, { phase: "done", pct: 100 });
return R.success(res, "Asset uploaded.", { data: asset }, 201);
} catch (err) {
console.error("[ASSET][UPLOAD]", err);
if (uploadId) uploadProgress.complete(uploadId, { phase: "error" });
if (err.status) return R.error(res, err.message, err.status, { detail: err.chibiBody });
return R.error(res, "Internal server error.", 500);
}
};
// ─── UPLOAD PROGRESS (SSE) ──────────────────────────────────────────────────
//
// Client opens this before POSTing the file, correlated by a client-generated
// uploadId sent as a form field on the upload request itself. Streams the
// real Express -> Garage httpUploadProgress events from s3.service.js's
// Upload — not a simulated or estimated number.
//
exports.streamUploadProgress = (req, res) => {
uploadProgress.subscribe(req.params.uploadId, res);
};
// ─── UPLOAD (batch) ─────────────────────────────────────────────────────────
//
// Accepts multiple files under the "files" field in one multipart request,
@@ -17,14 +17,12 @@ exports.getUsersDashboard = async (req, res) => {
const [
totalUsers,
activeUsers,
verifiedUsers,
archivedUsers,
accTypeBreakdown,
regTypeBreakdown,
] = await Promise.all([
mdl_Users.count({ paranoid: false }),
mdl_Users.count({ where: { is_active: true } }),
mdl_Users.count({ where: { is_verified: true } }),
mdl_Users.count({ where: { deletedAt: { [Op.ne]: null } }, paranoid: false }),
mdl_Users.findAll({
attributes: ['acc_type', [fn('COUNT', col('user_id')), 'count']],
@@ -46,8 +44,6 @@ exports.getUsersDashboard = async (req, res) => {
{ key: 'total', label: 'Total Users', value: totalUsers },
{ key: 'active', label: 'Active Users', value: activeUsers },
{ key: 'inactive', label: 'Inactive Users', value: totalUsers - archivedUsers - activeUsers },
{ key: 'verified', label: 'Verified', value: verifiedUsers },
{ key: 'archived', label: 'Archived', value: archivedUsers },
],
breakdowns: [
{
@@ -30,7 +30,15 @@ function buildWhere(query, extraWhere = {}) {
if (query.from || query.to) {
where.created_at = {};
if (query.from) where.created_at[Op.gte] = new Date(query.from);
if (query.to) where.created_at[Op.lte] = new Date(query.to);
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;
+60 -2
View File
@@ -34,10 +34,48 @@ const { excludeAttributes: usersExclude, jsonbSchemas: usersSchemas, computedAtt
const EXCLUDED = ['password', 'otp_code', 'otp_expires_at', 'must_change_password', 'password_expires_at'];
const auditByFields = ['createdBy', 'updatedBy', 'deletedBy'];
// ─── GROUPS FILTER ────────────────────────────────────────────────────────────
//
// "groups" is a M2M association, not a real column on `users` — buildQuery's
// generic Sequelize.col()-based filtering can't touch it. Pull any `groups`
// filter out of the request's filter list (so paginate() never sees a field
// it can't resolve) and turn it into an EXISTS subquery instead. EXISTS keeps
// the LEFT JOIN group data on each row intact (all of a user's groups still
// show up), while only matching users who belong to at least one of the
// selected groups.
function extractGroupsFilter(req) {
let filters = [];
try { filters = JSON.parse(req.query.filters || '[]'); } catch { filters = []; }
const groupsFilter = filters.find((f) => f.id === 'groups');
const remaining = filters.filter((f) => f.id !== 'groups');
req.query.filters = JSON.stringify(remaining);
if (!groupsFilter?.value) return null;
const groupIds = (Array.isArray(groupsFilter.value) ? groupsFilter.value : [groupsFilter.value])
.map((v) => parseInt(v, 10))
.filter((v) => !Number.isNaN(v));
return groupIds.length ? groupIds : null;
}
function groupsExistsWhere(groupIds) {
if (!groupIds) return undefined;
return Sequelize.literal(`EXISTS (
SELECT 1 FROM "user_group_members" ugm
WHERE ugm.user_id = "User"."user_id"
AND ugm."deletedAt" IS NULL
AND ugm.group_id IN (${groupIds.join(',')})
)`);
}
// ─── GET ALL ──────────────────────────────────────────────────────────────────
exports.getUsers = async (req, res) => {
try {
const groupIds = extractGroupsFilter(req);
const result = await paginate(mdl_Users, req, {
excludeAttributes: usersExclude,
jsonbSchemas: usersSchemas,
@@ -46,6 +84,7 @@ exports.getUsers = async (req, res) => {
context: "list",
auditOptions: { mdl_Users, parentAlias: 'User' },
findOptions: {
where: groupsExistsWhere(groupIds),
include: [{
model: mdl_UserGroups,
as: 'groups',
@@ -470,18 +509,37 @@ exports.terminateSession = async (req, res) => {
// ─── FIELD VALUES ─────────────────────────────────────────────────────────────
exports.getUserFieldValues = getFieldValues(mdl_Users, "USER", {
const getUserFieldValuesBase = getFieldValues(mdl_Users, "USER", {
blockedFields: ["password", "otp_code", "otp_expires_at", "personal_info"],
extraDateFields: ["modifiedAt", "ban_expires_at"],
allowJsonb: true,
selfJoin: true,
});
// "groups" isn't a Users model attribute, so the generic getFieldValues()
// helper can't resolve it — special-case it here, delegate everything else.
exports.getUserFieldValues = async (req, res) => {
if (req.query.field === 'groups') {
const groups = await mdl_UserGroups.findAll({
attributes: [['group_id', 'value'], ['name', 'label']],
order: [['name', 'ASC']],
raw: true,
});
return R.success(res, 'Field values retrieved.', groups);
}
return getUserFieldValuesBase(req, res);
};
// ─── ARCHIVED ─────────────────────────────────────────────────────────────────
// ─── GET ARCHIVED USERS ───────────────────────────────────────────────────────
exports.getArchivedUsers = async (req, res) => {
try {
const groupIds = extractGroupsFilter(req);
const archivedWhere = { deletedAt: { [Op.ne]: null }, is_active: false };
const groupsWhere = groupsExistsWhere(groupIds);
const result = await paginate(mdl_Users, req, {
excludeAttributes: usersExclude,
jsonbSchemas: usersSchemas,
@@ -491,7 +549,7 @@ exports.getArchivedUsers = async (req, res) => {
auditOptions: { mdl_Users, parentAlias: 'User' },
findOptions: {
paranoid: false,
where: { deletedAt: { [Op.ne]: null }, is_active: false },
where: groupsWhere ? { [Op.and]: [archivedWhere, groupsWhere] } : archivedWhere,
include: [{
model: mdl_UserGroups,
as: 'groups',