!doctype html> html lang="th"> head> meta charset="utf-8"> meta name="viewport" content="width=device-width,initial-scale=1"> title>BLUE CORNER — v2.8 Real Auth Security Harness/title> style> body{font-family:system-ui,sans-serif;margin:0;background:#f4f7fb;color:#102033} .wrap{max-width:820px;margin:auto;padding:18px} .card{background:#fff;border-radius:18px;padding:18px;margin:14px 0;box-shadow:0 4px 18px #0b3b6b12} input{width:100%;box-sizing:border-box;padding:14px;margin:6px 0 10px;border:1px solid #ccd6e2;border-radius:12px;font-size:16px} button{border:0;border-radius:12px;padding:13px 16px;margin:5px 4px 5px 0;background:#1268b3;color:#fff;font-size:15px;font-weight:700} .danger{background:#a92727}.secondary{background:#536273}.ok{color:#087a45;font-weight:700}.warn{color:#9b6900;font-weight:700}.bad{color:#a92727;font-weight:700} .muted{color:#66768a}.log{white-space:pre-wrap;background:#0e1b2b;color:#dcecff;padding:14px;border-radius:12px;min-height:160px;font-size:13px;overflow:auto} .row{display:flex;gap:8px;flex-wrap:wrap}.test{border-top:1px solid #e2e8f0;padding:10px 0}.pass{color:#087a45}.fail{color:#a92727} small{color:#66768a} /style> /head> body> div class="wrap"> h1>BLUE CORNER — v2.8 Security Harness/h1> p class="muted">Development/Test only • Real Supabase Auth • ไม่ใช้ service-role key • ไม่แตะ Live Stripe/p> div class="card"> b>Test Account/b> input id="email" type="email" placeholder="อีเมลสำหรบั Test Account"> input id="password" type="password" placeholder="รหสผ่นอย่างน้อย 8 ตว"> div class="row"> button id="signup">1. Create Real Test Account/button> button id="login">2. Sign in/button> button id="logout" class="danger">3. Logout/button> /div> /div> div class="card"> b>Authenticated API Smoke/b> div class="row"> button id="me">Verify API /me/button> button id="catalog">Catalog/button> button id="draft">Create Draft/button> button id="confirm">Confirm Order/button> button id="safety">Safety/button> button id="beginpay">Begin Payment/button> /div> /div> div class="card"> b>v2.8 Evidence Tests/b> div class="test">b>4. Logout → reuse old session/b>/br>small>จับ access token กอน Logout แลวเรยก /me หลัง Logout ตองถูกปฏสิทธ/Small>/br>button id="reuse">Run Test 4/button>span id="r4"/span/div> div class="test">b>5. Session / identity boundary/b>/br>small>ตรวจวาการเรยก API ตองผูกกบั authenticated session จริง/Small>/br>button id="identity">Run Test 5/button>span id="r5"/span/div> div class="test">b>6–7. CSRF missing / invalid/b>/br>small>POST ตองถูกปฏสิทธเมอไมมหรือม CSRF token ผิด/Small>/br>button id="csrf">Run Tests 6–7/button>span id="r67"/span/div> div class="test">b>8. Client role spoof/b>/br>small>สง role ปลอมจาก client และตรวจวา server ไมเชื่อบถอถือ/Small>/br>button id="rolespoof">Run Test 8/button>span id="r8"/span/div> div class="test">b>9. Client permission spoof/b>/br>small>สง permission ปลอมจาก client และตรวจวา server ไมเชื่อบถอถือ/Small>/br>button id="perms">Run Test 9/button>span id="r9"/span/div> div class="test">b>10. Barista → Manager API/b>/br>small>ตองใชบัญช Barista จรเพือพสิจณ 403; UI จะไมสร้างสิทธิปลอม/Small>/br>button id="managerdeny">Run Test 10/button>span id="r10"/span/div> div class="test">b>11. Staff A → Staff B identity spoof/b>/br>small>ตองมบญช Staff ที่ระบบกำหนดจรเพื่อกำหนด cross-identity test/Small>/br>button id="staffspoof">Run Test 11/button>span id="r11"/span/div> /div> div class="card"> b>Status/b> div id="status" class="warn">ยังไมได Login/div> p id="ids" class="muted"/p> /div> div class="card"> b>Log/b> div id="log" class="log">พรอมทดสอบ/div> /div> /div> script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"/script> script> const SUPABASE_URL="https://fkqqhjsghpjbkmnikoyk.supabase.co"; const KEY="sb_publishable_V3Gnuq_UdW7ocNaCgLjClg_ongvO0p3"; const STORE_ID="7ae2102b-eb94-4ab8-8b90-273167d06530"; const API=SUPABASE_URL+"/functions/v1/blue-corner-api"; const sb=supabase.createClient(SUPABASE_URL,KEY); let session=null,oldAccessToken=null,orderId=null,paymentId=null,csrfToken=null; const $=id=>document.getElementById(id); function log(x,ok){ $("log").textContent+="\n"+(ok===true?"✓ ":ok===false?"✗ ":"• ")+x; $("log").scrollTop=$("log").scrollHeight; } function status(x,ok){ $("status").textContent=x; $("status").className=ok?"ok":"warn"; } function ids(){ $("ids").textContent="user="+(session?.user?.id||"-")+ "\norder="+(orderId||"-")+"\npayment_operation="+(paymentId||"-"); } function key(){return crypto.randomUUID?crypto.randomUUID():Date.now()+"-"+Math.random().toString(36).slice(2)} async function issueCsrf(){ if(!session) throw Error("NO_SESSION"); const r=await fetch(API+"/csrf",{headers:{apikey:KEY,Authorization:"Bearer "+session.access_token}}); const j=await r.json().catch(()=>({})); if(!r.ok||!j.data?.token) throw Error("CSRF_INIT_"+(j.code||r.status)); csrfToken=j.data.token; log("CSRF token issued for current session",true); } async function signup(){ try{ const email=$("email").value.trim(),password=$("password").value; if(!email||!password)throw Errorกรอก Email และ Password กรอง"); if(password.length8)throw Error("Password ตองอย่างน้อย 8 ตัว"); const {data,error}=await sb.auth.signUp({email,password,emailRedirectTo:"https://fkqqhjsghpjbkmnikoyk.supabase.co/functions/v1/blue-corner-e2e-harness"}); if(error)throw error; if(data.session){ session=data.session; oldAccessToken=session.access_token; await issueCsrf(); status("Real Auth: SIGNED IN",true); logสร้างบัญชีแล้วได้ session แล้ว",true); }else logสร้างบัญชีแล้ว — ถ้ามี Email Confirmation ให้อนุมานอีเมลก่อน Sign in",true); ids(); }catch(e){log("CREATE: "+e.message,false)} } async function login(){ try{ const {data,error}=await sb.auth.signInWithPassword({email:$("email").value.trim(),password:$("password").value}); if(error)throw error; session=data.session;if(!session)throw Error("NO_SESSION"); oldAccessToken=session.access_token;await issueCsrf(); status("Real Auth: SIGNED IN",true);log("Login สำเร็จ + CSRF พร้อม",true);ids(); }catch(e){status("Login ไม่สำเร็จ",false);log("LOGIN: "+e.message,false)} } async function api(path,opt={}){ if(!session)throw Error("กรุณา Login ก่อน"); const h={apikey:KEY,Authorization:"Bearer "+session.access_token,"Content-Type":"application/json",...(opt.headers||{})}; if((opt.method||"GET").toUpperCase()==="POST"){ if(!csrfToken)throw Error("CSRF_NOT_READY"); h["x-csrf-token"]=csrfToken; } const r=await fetch(API+path,{...opt,headers:h}); const j=await r.json().catch(()=>({})); if(!r.ok)throw Object.assign(Error(j.code||j.reason||("HTTP_"+r.status)),{http:r.status,body:j}); log(path+" → HTTP "+r.status,true);return j; } async function me(){try{await api("/me");log("API Auth ผ่าน",true)}catch(e){log(e.message,false)}} async function catalog(){try{const j=await api("/catalog?store_id="+STORE_ID);log("Catalog products="+(j.data?.products?.length||0),true)}catch(e){log(e.message,false)}} async function draft(){try{ const c=await api("/catalog?store_id="+STORE_ID),p=c.data.products.find(x=>x.status==="ACTIVE"); if(!p)throw Error("NO_ACTIVE_PRODUCT"); const r=await api("/orders/draft",{method:"POST",headers:{"idempotency-key":key()},body:JSON.stringify({store_id:STORE_ID,items:[{product_id:p.id,quantity:1}]})}); orderId=r.data?.order_id||r.order_id;if(!orderId)throw Error("ORDER_ID_MISSING");log("Draft="+orderId,true);ids(); }catch(e){log(e.message,false)}} async function confirmOrder(){try{if(!orderId)throw Error("NO_ORDER");await api("/orders/confirm-direct",{method:"POST",headers:{"idempotency-key":key()},body:JSON.stringify({order_id:orderId})});log("Customer Confirmed",true)}catch(e){log(e.message,false)}} async function safety(){try{if(!orderId)throw Error("NO_ORDER");const j=await api("/orders/safety?order_id="+encodeURIComponent(orderId));log("Safety="+(j.safety_status||j.data?.safety_status),true)}catch(e){log(e.message,false)}} async function beginpay(){try{if(!orderId)throw Error("NO_ORDER");const j=await api("/orders/payment/begin",{method:"POST",headers:{"idempotency-key":key()},body:JSON.stringify({order_id:orderId})});paymentId=j.data?.payment_operation_id||j.payment_operation_id;log("Payment Operation="+paymentId,true);ids()}catch(e){log(e.message,false)}} async function testReuse(){ const out=$("r4");out.textContent=" ⏳"; if(!session){out.textContent=" ❌ Login ก่อน";return} const token=session.access_token; try{ await sb.auth.signOut({scope:"global"}); const r=await fetch(API+"/me",{headers:{apikey:KEY,Authorization:"Bearer "+token}}); if(r.status===401||r.status===403){out.textContent=" ✅ PASS ("+r.status+")";log("TEST 4 PASS: old session rejected after logout",true)} else{out.textContent=" ❌ FAIL ("+r.status+")";log("TEST 4 FAIL: old session still accepted",false)} }catch(e){out.textContent=" ❌ ERROR";log("TEST 4 ERROR: "+e.message,false)} finally{session=null;csrfToken=null;status("Logged out",true);ids()} } async function testIdentity(){ const out=$("r5");out.textContent=" ⏳"; try{ const r=await fetch(API+"/me",{headers:{apikey:KEY}}); if(r.status===401||r.status===403){out.textContent=" ✅ PASS ("+r.status+")";log("TEST 5 PASS: unauthenticated identity rejected",true)} else{out.textContent=" ❌ FAIL ("+r.status+")";log("TEST 5 FAIL: unauthenticated request accepted",false)} }catch(e){out.textContent=" ❌ ERROR";log("TEST 5 ERROR: "+e.message,false)} } async function testCsrf(){ const out=$("r67");out.textContent=" ⏳"; if(!session){out.textContent=" ❌ Login ก่อน";return} let a=false,b=false; try{ const base={apikey:KEY,Authorization:"Bearer "+session.access_token,"Content-Type":"application/json"}; const r1=await fetch(API+"/orders/draft",{method:"POST",headers:{...base,"idempotency-key":key()},body:JSON.stringify({store_id:STORE_ID,items:[]})}); a=(r1.status===403); const r2=await fetch(API+"/orders/draft",{method:"POST",headers:{...base,"x-csrf-token":"invalid-token","idempotency-key":key()},body:JSON.stringify({store_id:STORE_ID,items:[]})}); b=(r2.status===403); if(a&&b){out.textContent=" ✅ PASS";log("TEST 6–7 PASS: missing + invalid CSRF rejected",true)} else{out.textContent=" ❌ FAIL ("+r1.status+"/"+r2.status+")";log("TEST 6–7 FAIL: CSRF boundary did not reject as expected",false)} }catch(e){out.textContent=" ❌ ERROR";log("TEST 6–7 ERROR: "+e.message,false)} } async function testSpoof(kind){ const id=kind==="role"?"r8":"r9";const out=$(id);out.textContent=" ⏳"; if(!session){out.textContent=" ❌ Login ก่อน";return} try{ const h={"apikey":KEY,Authorization:"Bearer "+session.access_token,"Content-Type":"application/json","x-csrf-token":csrfToken}; h[kind==="role"?"x-role":"x-permission"]=kind==="role"?"OWNER":"STAFF_MANAGE"; const r=await fetch(API+"/me",{headers:h}); if(r.ok){out.textContent=" ✅ PASS";log("TEST "+(kind==="role"?8:9)+" PASS: spoof header did not replace authenticated identity",true)} else if(r.status===401||r.status===403){out.textContent=" ✅ PASS ("+r.status+")";log("TEST "+(kind==="role"?8:9)+" PASS: spoof rejected",true)} else{out.textContent=" ❌ FAIL ("+r.status+")";log("TEST "+(kind==="role"?8:9)+" unexpected response",false)} }catch(e){out.textContent=" ❌ ERROR";log("TEST ERROR: "+e.message,false)} } function unavailable(id,n){$(id).textContent=" 🟡 Requires real "+n+" fixture";log("TEST "+n+" not executed: required real account/role fixture is not available in this browser session","")} $("signup").onclick=signup;$("login").onclick=login;$("logout").onclick=async()=>{try{await sb.auth.signOut({scope:"global"})}finally{session=null;csrfToken=null;status("Logged out",true);log("Logout global สำเร็จ",true);ids()}}; $("me").onclick=me;$("catalog").onclick=catalog;$("draft").onclick=draft;$("confirm").onclick=confirmOrder;$("safety").onclick=safety;$("beginpay").onclick=beginpay; $("reuse").onclick=testReuse;$("identity").onclick=testIdentity;$("csrf").onclick=testCsrf;$("rolespoof").onclick=()=>testSpoof("role");$("perms").onclick=()=>testSpoof("permission"); $("managerdeny").onclick=()=>unavailable("r10","10 — real Barista + Manager endpoint");$("staffspoof").onclick=()=>unavailable("r11","11 — Staff A/B identity fixture"); log("Harness loaded — local/static mode",true); /script> /body> /html>