mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user