ce98dcaf27
Home layout (gnuboard-style widget configurator):
- /admin/eyoom/main-layout: toggle each section, set order, edit board slugs
- public.app_settings.home_layout = { sections: [{ id, enabled, order, boards }] }
- app/page.tsx reads layout, renders sections in admin-defined order
- getFeaturedBoards now accepts override slug list (admin-controlled featured boards)
- 7 toggleable widgets: hero, statStrip, topWinners, hotBoards, quickAccess, boardSlots, liveActivity
Shop admin (영카트):
- /admin/shop/brands: brand-grouped item counts
- /admin/shop/couponzone: coupon-zone create/delete
- /admin/shop/buylist: per-member purchase totals (mb_id, count, sum)
Members admin:
- /admin/members/visits: 60-day bar chart (g5_visit_sum)
- /admin/members/poll: g5_poll create / toggle use
Boards admin:
- /admin/boards/popular: 30-day search-keyword heatmap (g5_popular)
Eyoom admin:
- /admin/eyoom/tags: g5_eyoom_tag menu-display toggle / delete
- /admin/eyoom/attendance: top 100 attendance + 30 latest
SMS admin:
- /admin/sms/history: paginated 50/page sms5_history
SEO:
- /admin/seo: ask_seo_url meta CRUD
verify-cross: now exercises 38 admin URLs as admin (was 27)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
227 lines
9.5 KiB
JavaScript
227 lines
9.5 KiB
JavaScript
#!/usr/bin/env node
|
|
// Cross-verify PHP gnuboard clone (port 80, default vhost) and React app
|
|
// (path /react). Same DB underneath (MariaDB clone vs PG slot — both restored
|
|
// from the same 207 dump), so headline counts should match.
|
|
//
|
|
// Usage:
|
|
// ITERATIONS=5 node scripts/verify-cross.mjs
|
|
const HOST = process.env.HOST || 'http://103.31.14.201';
|
|
const PHP = process.env.PHP_BASE || HOST;
|
|
const REACT = process.env.REACT_BASE || (HOST + ':8088');
|
|
const ITER = Number(process.env.ITERATIONS || 5);
|
|
const PHP_USER = process.env.PHP_USER || 'admin';
|
|
const PHP_PASS = process.env.PHP_PASS || 'clone1234';
|
|
const REACT_USER = process.env.REACT_USER || 'testlogin';
|
|
const REACT_PASS = process.env.REACT_PASS || 'test1234';
|
|
|
|
let pass = 0, fail = 0;
|
|
const failures = [];
|
|
async function check(label, fn) {
|
|
try {
|
|
const ok = await fn();
|
|
if (ok) { pass++; console.log(` ✅ ${label}`); }
|
|
else { fail++; console.log(` ❌ ${label}`); failures.push(label); }
|
|
} catch (e) {
|
|
fail++; console.log(` ❌ ${label} (threw: ${e.message})`);
|
|
failures.push(`${label} (${e.message})`);
|
|
}
|
|
}
|
|
|
|
async function fetchOk(url, opts = {}) {
|
|
const r = await fetch(url, { redirect: 'manual', ...opts });
|
|
return r;
|
|
}
|
|
|
|
let phpCookie = '', reactCookie = '';
|
|
function takeCookie(jar, resp) {
|
|
const c = resp.headers.get('set-cookie');
|
|
if (!c) return jar;
|
|
const parts = c.split(',').map(s => s.split(';')[0]).filter(Boolean);
|
|
let cur = jar;
|
|
for (const p of parts) {
|
|
const eq = p.indexOf('=');
|
|
if (eq < 0) continue;
|
|
const name = p.slice(0, eq).trim();
|
|
const val = p.slice(eq + 1).trim();
|
|
const others = cur.split('; ').filter(s => s && !s.startsWith(name + '='));
|
|
others.push(`${name}=${val}`);
|
|
cur = others.join('; ');
|
|
}
|
|
return cur;
|
|
}
|
|
|
|
async function iteration(i) {
|
|
console.log(`\n=== ITERATION ${i} of ${ITER} ===`);
|
|
phpCookie = ''; reactCookie = '';
|
|
|
|
// --- PHP side ---
|
|
await check('[PHP] GET / (homepage 200)', async () => {
|
|
const r = await fetchOk(PHP + '/');
|
|
return r.status === 200 || r.status === 308;
|
|
});
|
|
await check('[PHP] GET /bbs/board.php?bo_table=free (200)', async () => {
|
|
const r = await fetchOk(PHP + '/bbs/board.php?bo_table=free');
|
|
return r.status === 200 || r.status === 308;
|
|
});
|
|
await check('[PHP] POST /bbs/login_check.php as admin', async () => {
|
|
const r = await fetchOk(PHP + '/bbs/login_check.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Cookie': phpCookie, 'Referer': PHP + '/bbs/login.php' },
|
|
body: new URLSearchParams({ url: '', mb_id: PHP_USER, mb_password: PHP_PASS, auto_login: '' }).toString(),
|
|
});
|
|
phpCookie = takeCookie(phpCookie, r);
|
|
return r.status === 302 || r.status === 303 || r.status === 200;
|
|
});
|
|
await check('[PHP] GET /adm/ as admin', async () => {
|
|
const r = await fetchOk(PHP + '/adm/', { headers: { Cookie: phpCookie } });
|
|
if (r.status !== 200) return false;
|
|
const t = await r.text();
|
|
return /관리자|admin|회원|level/i.test(t);
|
|
});
|
|
|
|
// --- React side ---
|
|
await check('[REACT] GET / (homepage 200)', async () => {
|
|
const r = await fetchOk(REACT + '/');
|
|
return r.status === 200 || r.status === 308;
|
|
});
|
|
await check('[REACT] GET /free (board 200/307/308 — read-level guard OK)', async () => {
|
|
const r = await fetchOk(REACT + '/free');
|
|
return r.status === 200 || r.status === 307 || r.status === 308;
|
|
});
|
|
await check('[REACT] POST /api/auth/login as testlogin', async () => {
|
|
const r = await fetchOk(REACT + '/api/auth/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Cookie': reactCookie },
|
|
body: new URLSearchParams({ loginId: REACT_USER, password: REACT_PASS }).toString(),
|
|
});
|
|
reactCookie = takeCookie(reactCookie, r);
|
|
return r.status === 303 || r.status === 302;
|
|
});
|
|
await check('[REACT] GET /mypage as testlogin', async () => {
|
|
const r = await fetchOk(REACT + '/mypage', { headers: { Cookie: reactCookie } });
|
|
return r.status === 200 || r.status === 308;
|
|
});
|
|
await check('[REACT] GET /shop (200)', async () => {
|
|
const r = await fetchOk(REACT + '/shop');
|
|
return r.status === 200 || r.status === 308;
|
|
});
|
|
await check('[REACT] GET /games/fortunes/play (auth-required, 307)', async () => {
|
|
const r = await fetchOk(REACT + '/games/fortunes/play');
|
|
return r.status === 307 || r.status === 200 || r.status === 308;
|
|
});
|
|
for (const t of ['basic', 'eyoom', 'amina', 'youngcart']) {
|
|
await check(`[THEME] /api/ui/theme?t=${t} sets cookie`, async () => {
|
|
const r = await fetchOk(REACT + `/api/ui/theme?t=${t}`);
|
|
const cs = r.headers.get('set-cookie') || '';
|
|
return cs.includes(`slot_theme=${t}`);
|
|
});
|
|
}
|
|
|
|
// --- Cross-stack invariants (data parity) ---
|
|
// PHP and React see the same MariaDB-vs-PG-restored data; counts on
|
|
// public-facing pages should match within +/- 1 (timing of restore).
|
|
await check('[CROSS] PHP /robots.txt + React /robots.txt both block all bots', async () => {
|
|
const a = await fetchOk(PHP + '/robots.txt');
|
|
const b = await fetchOk(REACT + '/robots.txt');
|
|
if (a.status !== 200 || b.status !== 200) return false;
|
|
const at = await a.text(); const bt = await b.text();
|
|
return /Disallow: \//.test(at) && /Disallow: \//.test(bt);
|
|
});
|
|
|
|
await check('[REACT] POST /api/auth/logout', async () => {
|
|
const r = await fetchOk(REACT + '/api/auth/logout', { method: 'POST', headers: { Cookie: reactCookie } });
|
|
return r.status === 303 || r.status === 302;
|
|
});
|
|
|
|
// ── ADMIN parity: log in as admin on both sides and walk admin pages ──
|
|
let phpAdminCookie = '';
|
|
function takePhpCookie(resp) {
|
|
const c = resp.headers.get('set-cookie');
|
|
if (!c) return;
|
|
const parts = c.split(',').map(s => s.split(';')[0]).filter(Boolean);
|
|
for (const p of parts) {
|
|
const eq = p.indexOf('=');
|
|
if (eq < 0) continue;
|
|
const name = p.slice(0, eq).trim();
|
|
const val = p.slice(eq + 1).trim();
|
|
const others = phpAdminCookie.split('; ').filter(s => s && !s.startsWith(name + '='));
|
|
others.push(`${name}=${val}`);
|
|
phpAdminCookie = others.join('; ');
|
|
}
|
|
}
|
|
|
|
await check('[PHP-ADMIN] POST login as admin/clone1234', async () => {
|
|
const a = await fetchOk(PHP + '/bbs/login.php', { headers: { Cookie: phpAdminCookie } }); takePhpCookie(a);
|
|
const r = await fetchOk(PHP + '/bbs/login_check.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Cookie': phpAdminCookie, 'Referer': PHP + '/bbs/login.php' },
|
|
body: new URLSearchParams({ url: '', mb_id: 'admin', mb_password: 'clone1234', auto_login: '' }).toString(),
|
|
});
|
|
takePhpCookie(r);
|
|
return r.status === 302 || r.status === 303 || r.status === 200;
|
|
});
|
|
for (const p of ['/adm/', '/adm/config_form.php', '/adm/board_list.php', '/adm/member_list.php']) {
|
|
await check(`[PHP-ADMIN] GET ${p}`, async () => {
|
|
const r = await fetchOk(PHP + p, { headers: { Cookie: phpAdminCookie } });
|
|
return r.status === 200 || r.status === 302;
|
|
});
|
|
}
|
|
|
|
let reactAdminCookie = '';
|
|
function takeReactAdminCookie(resp) {
|
|
const c = resp.headers.get('set-cookie');
|
|
if (!c) return;
|
|
const parts = c.split(',').map(s => s.split(';')[0]).filter(Boolean);
|
|
for (const p of parts) {
|
|
const eq = p.indexOf('=');
|
|
if (eq < 0) continue;
|
|
const name = p.slice(0, eq).trim();
|
|
const val = p.slice(eq + 1).trim();
|
|
const others = reactAdminCookie.split('; ').filter(s => s && !s.startsWith(name + '='));
|
|
others.push(`${name}=${val}`);
|
|
reactAdminCookie = others.join('; ');
|
|
}
|
|
}
|
|
await check('[REACT-ADMIN] POST login as admin/test1234 (lv 12)', async () => {
|
|
const r = await fetchOk(REACT + '/api/auth/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Cookie': reactAdminCookie },
|
|
body: new URLSearchParams({ loginId: 'admin', password: 'test1234' }).toString(),
|
|
});
|
|
takeReactAdminCookie(r);
|
|
return r.status === 303 || r.status === 302;
|
|
});
|
|
const adminPaths = [
|
|
'/admin', '/admin/members', '/admin/boards', '/admin/themes',
|
|
'/admin/config/popups', '/admin/config/auth', '/admin/config/maintenance', '/admin/config/clean',
|
|
'/admin/boards/groups', '/admin/boards/faq', '/admin/boards/contents',
|
|
'/admin/shop/items', '/admin/shop/config', '/admin/shop/categories',
|
|
'/admin/shop/coupons', '/admin/shop/orders', '/admin/shop/sendcost', '/admin/shop/banners',
|
|
'/admin/eyoom/menu', '/admin/eyoom/yellowcard', '/admin/eyoom/managers', '/admin/eyoom/biz-info',
|
|
'/admin/sms/config', '/admin/sms/write', '/admin/sms/history',
|
|
'/admin/plugin/sns', '/admin/plugin/recaptcha',
|
|
'/admin/roulette', '/admin/lottery/winners',
|
|
'/admin/eyoom/main-layout', '/admin/eyoom/tags', '/admin/eyoom/attendance',
|
|
'/admin/shop/brands', '/admin/shop/couponzone', '/admin/shop/buylist',
|
|
'/admin/members/visits', '/admin/members/poll',
|
|
'/admin/boards/popular', '/admin/seo',
|
|
];
|
|
for (const p of adminPaths) {
|
|
await check(`[REACT-ADMIN] GET ${p}`, async () => {
|
|
const r = await fetchOk(REACT + p, { headers: { Cookie: reactAdminCookie } });
|
|
return r.status === 200 || r.status === 308;
|
|
});
|
|
}
|
|
}
|
|
|
|
(async () => {
|
|
console.log(`Cross-verify PHP vs React on ${HOST}, ${ITER} iterations`);
|
|
for (let i = 1; i <= ITER; i++) await iteration(i);
|
|
console.log(`\n=== TOTAL: ${pass} passed, ${fail} failed ===`);
|
|
if (fail > 0) {
|
|
console.log('Failures:'); for (const f of failures) console.log(' - ' + f);
|
|
process.exit(1);
|
|
}
|
|
process.exit(0);
|
|
})();
|