mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -4,9 +4,11 @@ const sequelize = require("../../config/db.config");
|
||||
const Advertisement = require("../../models/advertisements/advertisements.mdl");
|
||||
const mdl_Assets = require("../../models/assets/assets.mdl");
|
||||
const mdl_Users = require('../../models/users/users.mdl');
|
||||
const mediaToken = require("../../services/mediaToken.service");
|
||||
const R = require('../../utils/response.util');
|
||||
const { paginate } = require("../../utils/paginate.util");
|
||||
const { adminExclude, jsonbSchemas, computedAttributes } = require("../../models/advertisements/advertisements.attributes");
|
||||
const { PLACEMENT_MAP, PLACEMENT_KEYS } = require("../../models/advertisements/advertisements.placements");
|
||||
const { getFieldValues } = require("../../utils/fieldValues.util");
|
||||
const logActivity = require('../../utils/logActivity.util');
|
||||
|
||||
@@ -16,9 +18,32 @@ const { Op } = require('sequelize');
|
||||
|
||||
const notDeleted = { deletedAt: null };
|
||||
|
||||
const ALLOWED_TYPES = ["hero", "banner", "popup", "sidebar"];
|
||||
const ALLOWED_STATUSES = ["draft", "active", "scheduled", "expired", "archived"];
|
||||
|
||||
// Fields needed off the associated Asset to render a preview AND (for S3 assets)
|
||||
// mint a stream token — storage_key is stripped again in attachImageStreamToken
|
||||
// before the row is ever sent out.
|
||||
const AD_IMAGE_ATTRIBUTES = ["asset_id", "display_name", "file_url", "thumbnail_url", "storage_provider", "storage_key", "file_type", "mime_type"];
|
||||
|
||||
// ─── Media proxying ─────────────────────────────────────────────────────────
|
||||
// Mirrors controllers/admin/assets.controller.js's redactS3Url/attachStreamTokens.
|
||||
// Private (S3-backed) advertisement images must never expose a raw file_url to
|
||||
// the browser — mint a short-lived stream token instead so the frontend resolves
|
||||
// it through GET /api/client/media/stream/:token. Public/chibisafe images keep
|
||||
// their direct file_url (no proxy needed).
|
||||
async function attachImageStreamToken(image, req) {
|
||||
if (!image || image.storage_provider !== "s3" || !mediaToken.SUPPORTED_TYPES.includes(image.file_type)) {
|
||||
return image;
|
||||
}
|
||||
const ip = mediaToken.resolveIp(req);
|
||||
const { token } = await mediaToken.issueForAsset(image, req.user?.user_id, ip);
|
||||
image.stream_token = token;
|
||||
image.file_url = null;
|
||||
image.thumbnail_url = null;
|
||||
delete image.storage_key;
|
||||
return image;
|
||||
}
|
||||
|
||||
// ─── Status derivation ─────────────────────────────────────────────────────
|
||||
// status is never trusted as manually-set truth — it's derived from is_active
|
||||
// + start_date/end_date every time an advertisement is read or written.
|
||||
@@ -53,13 +78,17 @@ function normalizeCtas(ctas) {
|
||||
}
|
||||
|
||||
async function applyAdvertisementFields(advertisement, body) {
|
||||
if (body.type !== undefined) {
|
||||
if (!ALLOWED_TYPES.includes(body.type)) {
|
||||
const err = new Error(`Invalid type. Must be one of: ${ALLOWED_TYPES.join(", ")}`);
|
||||
// placement is the only settable "where" — type/format is always derived
|
||||
// from the placement's registry entry, never accepted directly from the body.
|
||||
if (body.placement !== undefined) {
|
||||
const entry = PLACEMENT_MAP[body.placement];
|
||||
if (!entry) {
|
||||
const err = new Error(`Invalid placement. Must be one of: ${PLACEMENT_KEYS.join(", ")}`);
|
||||
err.status = 400;
|
||||
throw err;
|
||||
}
|
||||
advertisement.type = body.type;
|
||||
advertisement.placement = body.placement;
|
||||
advertisement.type = entry.format;
|
||||
}
|
||||
|
||||
// status is intentionally NOT settable here — it's derived via deriveStatus()
|
||||
@@ -93,8 +122,8 @@ async function applyAdvertisementFields(advertisement, body) {
|
||||
if (body.is_active !== undefined) advertisement.is_active = body.is_active === true || body.is_active === "true";
|
||||
|
||||
if (body.size !== undefined) {
|
||||
if (body.size !== null && !["sm", "md", "lg"].includes(body.size)) {
|
||||
const err = new Error(`Invalid size. Must be one of: sm, md, lg`);
|
||||
if (body.size !== null && !["sm", "md", "lg", "xl"].includes(body.size)) {
|
||||
const err = new Error(`Invalid size. Must be one of: sm, md, lg, xl`);
|
||||
err.status = 400;
|
||||
throw err;
|
||||
}
|
||||
@@ -120,7 +149,7 @@ exports.getAdvertisements = async (req, res) => {
|
||||
include: [{
|
||||
model: mdl_Assets,
|
||||
as: "image",
|
||||
attributes: ["asset_id", "display_name", "file_url", "thumbnail_url"],
|
||||
attributes: AD_IMAGE_ATTRIBUTES,
|
||||
required: false,
|
||||
}],
|
||||
},
|
||||
@@ -129,7 +158,10 @@ exports.getAdvertisements = async (req, res) => {
|
||||
// Resync status on the way out — never trust what's stored, since
|
||||
// start_date/end_date may have lapsed since the row was last saved.
|
||||
if (Array.isArray(result?.data)) {
|
||||
result.data = result.data.map((row) => ({ ...row, status: deriveStatus(row) }));
|
||||
result.data = await Promise.all(result.data.map(async (row) => {
|
||||
if (row.image) await attachImageStreamToken(row.image, req);
|
||||
return { ...row, status: deriveStatus(row) };
|
||||
}));
|
||||
}
|
||||
|
||||
return R.success(res, "Advertisements retrieved.", result);
|
||||
@@ -149,7 +181,7 @@ exports.getAdvertisement = async (req, res) => {
|
||||
const advertisement = await Advertisement.findOne({
|
||||
where: { advertisement_id: advertisementId, ...notDeleted },
|
||||
include: [
|
||||
{ model: mdl_Assets, as: "image", attributes: ["asset_id", "display_name", "file_url", "thumbnail_url"], required: false },
|
||||
{ model: mdl_Assets, as: "image", attributes: AD_IMAGE_ATTRIBUTES, required: false },
|
||||
{ model: mdl_Users, as: "creator", attributes: ["user_id", "personal_info"], foreignKey: "createdBy" },
|
||||
{ model: mdl_Users, as: "updater", attributes: ["user_id", "personal_info"], foreignKey: "updatedBy" },
|
||||
],
|
||||
@@ -160,6 +192,8 @@ exports.getAdvertisement = async (req, res) => {
|
||||
const json = advertisement.toJSON();
|
||||
json.status = deriveStatus(json);
|
||||
|
||||
if (json.image) await attachImageStreamToken(json.image, req);
|
||||
|
||||
if (json.creator) {
|
||||
json.creator = {
|
||||
user_id: json.creator.user_id,
|
||||
@@ -184,20 +218,21 @@ exports.getAdvertisement = async (req, res) => {
|
||||
|
||||
exports.createAdvertisement = async (req, res) => {
|
||||
try {
|
||||
const { type, createdBy } = req.body;
|
||||
const { placement, createdBy } = req.body;
|
||||
|
||||
if (!type) return R.error(res, "type is required.", 400);
|
||||
if (!ALLOWED_TYPES.includes(type)) return R.error(res, `Invalid type. Must be one of: ${ALLOWED_TYPES.join(", ")}`, 400);
|
||||
if (!placement) return R.error(res, "placement is required.", 400);
|
||||
const entry = PLACEMENT_MAP[placement];
|
||||
if (!entry) return R.error(res, `Invalid placement. Must be one of: ${PLACEMENT_KEYS.join(", ")}`, 400);
|
||||
if (!createdBy) return R.error(res, "createdBy is required.", 400);
|
||||
|
||||
const t = await sequelize.transaction();
|
||||
try {
|
||||
const advertisement = await Advertisement.build({ type, createdBy });
|
||||
const advertisement = await Advertisement.build({ placement, type: entry.format, createdBy });
|
||||
await applyAdvertisementFields(advertisement, req.body);
|
||||
await advertisement.save({ transaction: t });
|
||||
await t.commit();
|
||||
|
||||
logActivity(req.user?.user_id, 'create_advertisement', { entityType: 'advertisement', entityId: advertisement.advertisement_id, details: { type: advertisement.type } });
|
||||
logActivity(req.user?.user_id, 'create_advertisement', { entityType: 'advertisement', entityId: advertisement.advertisement_id, details: { placement: advertisement.placement, type: advertisement.type } });
|
||||
return R.success(res, "Advertisement created.", { data: advertisement }, 201);
|
||||
} catch (dbErr) {
|
||||
try { await t.rollback(); } catch { /* connection gone */ }
|
||||
|
||||
Reference in New Issue
Block a user