#!/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); })();