!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>