assets and tier plans revamp

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-08-01 17:44:25 +08:00
parent 39c3c4566b
commit cae958b5d5
41 changed files with 1271 additions and 165 deletions
+1 -1
View File
@@ -64,7 +64,7 @@ async function resolveCourseUserIds(courseUuid) {
holders.forEach((h) => userIds.add(String(h.user_id)));
}
const product = await mdl_Product.findOne({ where: { course_id: course.course_id } });
const product = await mdl_Product.findOne({ where: { purchasable_type: 'course', purchasable_id: course.course_id } });
if (product) {
const purchasers = await mdl_CoursePurchase.findAll({
attributes: ['user_id'],
+45
View File
@@ -0,0 +1,45 @@
/***********************************************************************************************************************************************************************
* File Name: purchasable.util.js
* Type of Program: Utility
* Description: Resolves a Product's polymorphic target (purchasable_type +
* purchasable_id → Course | Unit | Lesson) and the checkout
* route for it. Products.mdl.js can't express a single
* Sequelize association across three different target models,
* so this is the shared lookup every consumer (access checks,
* admin product CRUD, client checkout) goes through instead.
*
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Aug. 1, 2026
***********************************************************************************************************************************************************************/
'use strict';
const { Course } = require('../models/courses/courses.mdl');
const Unit = require('../models/courses/units.mdl');
const Lesson = require('../models/courses/lessons.mdl');
const TYPE_MODELS = { course: Course, unit: Unit, lesson: Lesson };
const TYPE_PK = { course: 'course_id', unit: 'unit_id', lesson: 'lesson_id' };
function getPurchasableModel(type) {
return TYPE_MODELS[type] ?? null;
}
// Returns the target row (needs at least uuid/course_id + subscription for
// callers), or null if the type is unknown or the row doesn't exist.
async function resolvePurchasable(type, id) {
const Model = TYPE_MODELS[type];
if (!Model) return null;
return Model.findOne({ where: { [TYPE_PK[type]]: id, deletedAt: null } });
}
// Course checkout stays keyed by numeric course_id (existing convention);
// Unit/Lesson checkout is keyed by uuid, matching how /units/:uuid and
// /lessons/:uuid already work everywhere else in the standalone consumption paths.
function checkoutPath(type, record) {
if (type === 'course') return `/course/${record.course_id}/checkout`;
if (type === 'unit') return `/units/${record.uuid}/checkout`;
if (type === 'lesson') return `/lessons/${record.uuid}/checkout`;
return null;
}
module.exports = { getPurchasableModel, resolvePurchasable, checkoutPath };
+48
View File
@@ -0,0 +1,48 @@
// utils/resolveAvatar.util.js
//
// Resolves a stored avatar into a browser-usable URL at read time.
//
// personal_info.avatar.url is never trustworthy as stored:
// - S3-stored avatars (avatar.uuid present) were historically saved with a
// raw, unsigned bucket URL (see s3.service.js buildPublicUrl). The current
// Garage ingress (Cloudflare lane, see chibistar/Caddyfile) forwards reads
// straight to Garage with no re-signing, and Garage rejects anonymous
// requests outright — so that stored URL 403s in the browser. Even a
// presigned URL would go stale if persisted (getPublicUrl() expires in 4h),
// so the only correct fix is to mint a fresh one on every read from the
// stored key (avatar.uuid), never trust what's on the row.
// - Google-picture avatars (reg_type: 'google', no uuid — see
// auth.controller.js googleCallback) are an external CDN URL and pass
// through unchanged; there's nothing of ours to sign.
//
const { getPublicUrl } = require('../services/s3.service');
async function resolveAvatarUrl(avatar) {
if (!avatar) return avatar ?? null;
if (!avatar.uuid) return avatar; // external URL (e.g. Google) — nothing to sign
try {
const url = await getPublicUrl(avatar.uuid);
return { ...avatar, url };
} catch (err) {
console.error('[AVATAR] Failed to resolve presigned URL for', avatar.uuid, err);
return avatar; // fall back to the stored value rather than failing the whole response
}
}
// Mutates-and-returns a shallow copy of a user (plain object or Sequelize
// instance) with personal_info.avatar resolved. Safe to call on a user with
// no avatar at all.
async function resolveUserAvatar(user) {
if (!user) return user;
const u = user.toJSON ? user.toJSON() : { ...user };
if (!u.personal_info?.avatar) return u;
u.personal_info = {
...u.personal_info,
avatar: await resolveAvatarUrl(u.personal_info.avatar),
};
return u;
}
module.exports = { resolveAvatarUrl, resolveUserAvatar };