diff options
| author | Your Name <you@example.com> | 2026-05-15 22:27:14 +0530 |
|---|---|---|
| committer | Your Name <you@example.com> | 2026-05-15 22:27:14 +0530 |
| commit | 1263d86314fc0760d9be8eea415ccecbc047a5eb (patch) | |
| tree | 778130f0beb59d52f68e0e5f11388bf4b1470130 /main/captive_portal.c | |
| parent | a7d0a672d59bf8985a6fc0e61b49015fabd96513 (diff) | |
Phase 2 WIP: Cashu payment endpoints, session tracking, updated checklist
- Add cashu.c/h: Cashu token decode (cashuA/base64url), proof state check via mint API, allotment calculator
- Add session.c/h: time-based session management with allotment/expiry, spent secret tracking
- Add tollgate_api.c/h: HTTP server on :2121 with GET / (kind=10021 discovery), POST / (payment processing), /usage, /whoami
- Update captive portal HTML: replace Grant Free Access with Cashu token paste form + Pay & Connect button
- Update tollgate_main.c: wire in session manager, TollGate API, 1s session tick loop
- Add tests/phase2.mjs: Phase 2 test suite (discovery, invalid token, wrong mint, valid payment)
- Update CHECKLIST.md: reflect Phase 1 complete, Phase 2 in progress with known bugs
Known issues (not yet flashed):
- Stack overflow crash in httpd POST handler (need stack_size=16384 + heap allocations)
- cashu_decode_token uses 2KB stack buffer (needs heap alloc)
- Mint URL should be testnut.cashu.space (nofee.testnut has API compat issues)
Diffstat (limited to 'main/captive_portal.c')
| -rw-r--r-- | main/captive_portal.c | 35 |
1 files changed, 22 insertions, 13 deletions
diff --git a/main/captive_portal.c b/main/captive_portal.c index acff9c2..17f672f 100644 --- a/main/captive_portal.c +++ b/main/captive_portal.c | |||
| @@ -38,6 +38,8 @@ static const char PORTAL_HTML[] = \ | |||
| 38 | "font-size:16px;font-weight:bold;cursor:pointer;width:100%;margin-top:8px}" | 38 | "font-size:16px;font-weight:bold;cursor:pointer;width:100%;margin-top:8px}" |
| 39 | ".btn:hover{background:#e8850f}" | 39 | ".btn:hover{background:#e8850f}" |
| 40 | ".btn:disabled{background:#333;color:#666;cursor:not-allowed}" | 40 | ".btn:disabled{background:#333;color:#666;cursor:not-allowed}" |
| 41 | "textarea{width:100%;height:80px;background:#252525;border:1px solid #333;border-radius:8px;" | ||
| 42 | "color:#fff;padding:12px;font-family:monospace;font-size:12px;margin-top:8px;resize:none}" | ||
| 41 | "</style>" | 43 | "</style>" |
| 42 | "</head><body>" | 44 | "</head><body>" |
| 43 | "<div class='card'>" | 45 | "<div class='card'>" |
| @@ -47,25 +49,32 @@ static const char PORTAL_HTML[] = \ | |||
| 47 | "<div class='price-amount' id='price'>Loading...</div>" | 49 | "<div class='price-amount' id='price'>Loading...</div>" |
| 48 | "<div class='price-unit'>sats per minute</div>" | 50 | "<div class='price-unit'>sats per minute</div>" |
| 49 | "</div>" | 51 | "</div>" |
| 50 | "<button class='btn' id='grantBtn' onclick='grantAccess()'>Grant Free Access</button>" | 52 | "<textarea id='tokenInput' placeholder='Paste your Cashu token here (cashuA...)'></textarea>" |
| 53 | "<button class='btn' id='payBtn' onclick='payToken()'>Pay & Connect</button>" | ||
| 51 | "<div id='status'></div>" | 54 | "<div id='status'></div>" |
| 52 | "</div>" | 55 | "</div>" |
| 53 | "<script>" | 56 | "<script>" |
| 54 | "const priceEl=document.getElementById('price');" | 57 | "const priceEl=document.getElementById('price');" |
| 55 | "const statusEl=document.getElementById('status');" | 58 | "const statusEl=document.getElementById('status');" |
| 56 | "const grantBtn=document.getElementById('grantBtn');" | 59 | "const payBtn=document.getElementById('payBtn');" |
| 57 | "fetch('/api/status').then(r=>r.json()).then(d=>{priceEl.textContent=d.price||'21';}).catch(()=>{priceEl.textContent='21';});" | 60 | "const tokenInput=document.getElementById('tokenInput');" |
| 61 | "fetch('http://192.168.4.1:2121/').then(r=>r.json()).then(d=>{" | ||
| 62 | "if(d.tags){const p=d.tags.find(t=>t[0]==='price_per_step');if(p)priceEl.textContent=p[2]||'21';}" | ||
| 63 | "}).catch(()=>{priceEl.textContent='21';});" | ||
| 58 | "function showStatus(msg,type){statusEl.textContent=msg;statusEl.className=type;}" | 64 | "function showStatus(msg,type){statusEl.textContent=msg;statusEl.className=type;}" |
| 59 | "function grantAccess(){" | 65 | "function payToken(){" |
| 60 | " grantBtn.disabled=true;" | 66 | "const token=tokenInput.value.trim();" |
| 61 | " showStatus('Connecting...','processing');" | 67 | "if(!token||!token.startsWith('cashuA')){showStatus('Please paste a valid Cashu token','error');return;}" |
| 62 | " fetch('/grant_access').then(r=>r.json()).then(d=>{" | 68 | "payBtn.disabled=true;" |
| 63 | " if(d.status==='granted'){" | 69 | "showStatus('Processing payment...','processing');" |
| 64 | " showStatus('Connected! You have internet access.','success');" | 70 | "fetch('http://192.168.4.1:2121/',{method:'POST',body:token}).then(r=>{" |
| 65 | " grantBtn.textContent='Connected!';" | 71 | "if(r.ok)return r.json();" |
| 66 | " setTimeout(()=>{window.location.href='http://detectportal.firefox.com/success.txt';},2000);" | 72 | "return r.json().then(d=>{throw new Error(d.content||'Payment failed');});" |
| 67 | " }else{showStatus('Error: '+d.message,'error');grantBtn.disabled=false;}" | 73 | "}).then(d=>{" |
| 68 | " }).catch(e=>{showStatus('Connection error','error');grantBtn.disabled=false;});" | 74 | "if(d.kind===1022){showStatus('Connected! You have internet access.','success');payBtn.textContent='Connected!';" |
| 75 | "setTimeout(()=>{window.location.href='http://detectportal.firefox.com/success.txt';},2000);}" | ||
| 76 | "else if(d.kind===21023){showStatus('Error: '+(d.content||'Unknown error'),'error');payBtn.disabled=false;}" | ||
| 77 | "}).catch(e=>{showStatus(e.message||'Connection error','error');payBtn.disabled=false;});" | ||
| 69 | "}" | 78 | "}" |
| 70 | "</script>" | 79 | "</script>" |
| 71 | "</body></html>"; | 80 | "</body></html>"; |