asset viewer now viewing on tabs

Tabs is better for this yay

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-08-28 19:34:10 +08:00
parent 5b784ca87c
commit 05f27e34d7
26 changed files with 1679 additions and 351 deletions
+34
View File
@@ -0,0 +1,34 @@
const { Course } = require('../../api/models/courses/courses.mdl');
const Unit = require('../../api/models/courses/units.mdl');
const Lesson = require('../../api/models/courses/lessons.mdl');
const { TaskList, Task } = require('../../api/models/task/task.mdl');
const Advertisement = require('../../api/models/advertisements/advertisements.mdl');
const TierPlan = require('../../api/models/tiers/tier_plans.mdl');
const { Op } = require('sequelize');
const QA_PREFIX = '[QA-TEST';
const TARGETS = [
{ model: Task, column: 'name' },
{ model: TaskList, column: 'name' },
{ model: Advertisement, column: 'headline' },
{ model: Unit, column: 'title' },
{ model: Lesson, column: 'title' },
{ model: Course, column: 'title' },
{ model: TierPlan, column: 'label' },
];
/** Hard-deletes every QA-TEST-prefixed fixture row across all modules this suite touches. */
async function cleanupQaTestFixtures() {
const removed = {};
for (const { model, column } of TARGETS) {
const count = await model.destroy({
where: { [column]: { [Op.like]: `${QA_PREFIX}%` } },
force: true, // hard delete, not just paranoid soft-delete — mirrors the manual "archive then permanently delete" cleanup flow
});
if (count) removed[model.name] = count;
}
return removed;
}
module.exports = { cleanupQaTestFixtures, QA_PREFIX };
+23
View File
@@ -0,0 +1,23 @@
// Reuses the app's own Sequelize connection/config instead of duplicating
// DB_HOST/DB_PORT/... wiring — env.js pre-loads apps/api/.env so db.config.js
// picks up the real dev credentials when it runs its own (no-op, already-set)
// dotenv.config() call.
require('./env');
const sequelize = require('../../api/config/db.config');
const Users = require('../../api/models/users/users.mdl');
async function getUserIdByEmail(email) {
const user = await Users.findOne({ where: { email }, attributes: ['user_id'] });
if (!user) throw new Error(`No user found for email ${email} — check the dev DB seed / helpers/env.js USERS config.`);
return String(user.user_id);
}
async function getPendingOtp(user_id) {
const rows = await sequelize.query(
`SELECT otp_code FROM users WHERE user_id = :uid`,
{ replacements: { uid: user_id }, type: sequelize.QueryTypes.SELECT },
);
return rows[0]?.otp_code ?? null;
}
module.exports = { sequelize, getUserIdByEmail, getPendingOtp };
+33
View File
@@ -0,0 +1,33 @@
// Loads real dev secrets from apps/api/.env (gitignored — never committed).
// Falls back to apps/api/.env-development (git-tracked shared dev creds) for
// anything .env doesn't override, matching how apps/api itself resolves config.
const path = require('path');
require('dotenv').config({ path: path.join(__dirname, '../../api/.env') });
require('dotenv').config({ path: path.join(__dirname, '../../api/.env-development') });
const APP_URL = process.env.E2E_APP_URL || 'http://localhost:5173';
const API_URL = process.env.E2E_API_URL || 'http://localhost:3024/api';
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;
if (!JWT_REFRESH_SECRET || JWT_REFRESH_SECRET.startsWith('CHANGE_ME')) {
throw new Error('JWT_REFRESH_SECRET missing/placeholder — check apps/api/.env has real dev secrets.');
}
// Known test accounts. user_id is intentionally NOT hardcoded here — these
// accounts get recreated over time (see chibistar/automation/starr/config.cjs
// history) which silently breaks hardcoded ids. helpers/session.js looks the
// id up by email at call time instead.
const USERS = {
admin: {
email: process.env.E2E_ADMIN_EMAIL || 'k80308392@gmail.com',
password: process.env.E2E_ADMIN_PASSWORD || 'Test123@',
acc_type: 'admin',
},
seedClient: {
// Synthetic, no real owner — safe default for read-only/client-facing checks.
email: process.env.E2E_SEED_CLIENT_EMAIL || 'veronica.castro@example.com',
acc_type: 'user',
},
};
module.exports = { APP_URL, API_URL, JWT_REFRESH_SECRET, USERS };
+55
View File
@@ -0,0 +1,55 @@
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const { APP_URL, JWT_REFRESH_SECRET, USERS } = require('./env');
const { sequelize, getUserIdByEmail, getPendingOtp } = require('./db');
/** Build the cookie jar for context.addCookies() from a refresh token. */
function buildCookies(refreshToken, csrfToken = 'e2e-csrf') {
return [
{ name: 'refreshToken', value: refreshToken, domain: 'localhost', path: '/', httpOnly: true, secure: false, sameSite: 'Lax' },
{ name: 'csrfToken', value: csrfToken, domain: 'localhost', path: '/', httpOnly: false, secure: false, sameSite: 'Lax' },
{ name: 'sidebar_state', value: 'false', domain: 'localhost', path: '/', httpOnly: false, secure: false, sameSite: 'Lax' },
];
}
/**
* Generate a fresh refresh token + DB session row for a client user, keyed
* by email (not a hardcoded user_id — those drift as test accounts get
* recreated, see chibistar/automation/starr/config.cjs history).
*/
async function generateClientSession(email) {
const user_id = await getUserIdByEmail(email);
const refreshToken = jwt.sign({ user_id }, JWT_REFRESH_SECRET, { expiresIn: '7d' });
const tokenHash = crypto.createHash('sha256').update(refreshToken).digest('hex');
await sequelize.query(
`INSERT INTO user_sessions (user_id, refresh_token_hash, is_active, login_info, "createdAt", "updatedAt")
VALUES (:uid, :hash, true, '{}', NOW(), NOW())`,
{ replacements: { uid: user_id, hash: tokenHash }, type: sequelize.QueryTypes.INSERT },
);
return { user_id, refreshToken };
}
/**
* Authenticate as admin via cookie injection (same mechanism as
* generateClientSession), not the UI login form.
*
* REAL FINDING (2026-08-25): the admin account (k80308392@gmail.com) is
* Google-OAuth-only now — submitting the UI email/password form returns
* "Login failed — Please log in with Google." This matches a prior finding
* that the *live*-domain admin session had gone Google-only, but it turns
* out to be true here against localhost too, so the email+password+OTP flow
* this helper originally ported from chibistar/automation/starr/config.cjs
* (loginAsAdmin) is dead — forging the session cookie is the only way in now.
*/
async function loginAsAdmin(page) {
const { refreshToken } = await generateClientSession(USERS.admin.email);
await page.context().addCookies(buildCookies(refreshToken));
await page.goto(`${APP_URL}/admin`, { waitUntil: 'networkidle' });
if (/\/login/.test(page.url())) {
throw new Error('Cookie-based admin auth bounced to /login — check JWT_REFRESH_SECRET / user_sessions wiring.');
}
}
module.exports = { buildCookies, generateClientSession, loginAsAdmin };
+37
View File
@@ -0,0 +1,37 @@
const { execSync } = require('child_process');
// Real monitor resolution instead of a hardcoded viewport — this suite runs
// on whatever box the self-hosted runner/dev machine happens to be (varies:
// 1366x768, 1920x1080, ...). Memoized per process.
let _cachedViewport = null;
function detectViewport() {
if (_cachedViewport) return _cachedViewport;
const FALLBACK = { width: 1366, height: 768 };
let detected = null;
try {
if (process.platform === 'linux') {
const out = execSync('xrandr --current 2>/dev/null', { encoding: 'utf8' });
const m = out.match(/(\d+)x(\d+)\s+[\d.]+\*/);
if (m) detected = { width: Number(m[1]), height: Number(m[2]) };
} else if (process.platform === 'win32') {
const out = execSync(
'powershell -NoProfile -Command "Add-Type -AssemblyName System.Windows.Forms; ' +
'$b = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds; Write-Output \\"$($b.Width)x$($b.Height)\\""',
{ encoding: 'utf8', windowsHide: true },
).trim();
const m = out.match(/(\d+)x(\d+)/);
if (m) detected = { width: Number(m[1]), height: Number(m[2]) };
} else if (process.platform === 'darwin') {
const out = execSync('system_profiler SPDisplaysDataType 2>/dev/null', { encoding: 'utf8' });
const m = out.match(/Resolution:\s*(\d+)\s*x\s*(\d+)/);
if (m) detected = { width: Number(m[1]), height: Number(m[2]) };
}
} catch {
// No xrandr on a headless CI box, PowerShell blocked, etc. — fall back.
}
_cachedViewport = detected ?? FALLBACK;
return _cachedViewport;
}
module.exports = { detectViewport };