v1.1 security release: PBKDF2 passwords, session expiry, rate limiting, SSE streaming
- store.py: passwords now PBKDF2-HMAC-SHA256 (390k iters, random salt,
constant-time compare). Legacy unsalted-SHA256 hashes upgrade transparently
on successful login — zero impact on existing users.
- Sessions expire after 30 days (lazy cleanup on token lookup).
- Rate limiting on register (5/hr/IP), login (10/15min per IP AND per
username), chat (30/hr/IP) — SQLite-backed, shared across gunicorn workers.
- Registration now requires 8+ char passwords.
- New /api/chat/stream SSE endpoint: citations first ('meta'), streamed
answer deltas, 'done'. Client auto-falls back to non-streaming.
- nginx: proxy_buffering off for SSE.
This commit is contained in:
@@ -316,16 +316,46 @@
|
||||
async function speak(text){
|
||||
try{ const r=await fetch('/api/speak',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text:text.replace(/\n+/g,'. ').substring(0,2500),voice:mySettings.tts_voice||'aria'})}); const b=await r.blob(); new Audio(URL.createObjectURL(b)).play(); }catch(e){}
|
||||
}
|
||||
function liveBubble(){
|
||||
const m=document.createElement('div'); m.className='msg agent';
|
||||
const l=document.createElement('div'); l.className='who-label'; l.textContent=(AGENTS.find(x=>x.id===active)?.name||'Attorney');
|
||||
const b=document.createElement('div'); b.className='bubble'; b.textContent='';
|
||||
m.appendChild(l); m.appendChild(b);
|
||||
document.getElementById('messages').appendChild(m);
|
||||
document.getElementById('messages').scrollTop=document.getElementById('messages').scrollHeight;
|
||||
return {m, b};
|
||||
}
|
||||
async function send(){
|
||||
const t=document.getElementById('input').value.trim(); if(!t)return;
|
||||
document.getElementById('input').value='';
|
||||
addBubble('user',t); history[active].push({role:'user',content:t});
|
||||
typing(true); document.getElementById('sendBtn').disabled=true;
|
||||
try{
|
||||
const r=await fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json',...authHeaders()},body:JSON.stringify({agent_id:active,message:t,history:history[active].slice(0,-1)})});
|
||||
const d=await r.json(); typing(false);
|
||||
if(d.answer){ addBubble('assistant',d.answer,d.citations); history[active].push({role:'assistant',content:d.answer,citations:d.citations}); }
|
||||
else addBubble('assistant',d.error||'Something went wrong.',null);
|
||||
const r=await fetch('/api/chat/stream',{method:'POST',headers:{'Content-Type':'application/json',...authHeaders()},body:JSON.stringify({agent_id:active,message:t,history:history[active].slice(0,-1)})});
|
||||
if(!r.ok || !r.body){ // fallback to non-streaming
|
||||
const d=await r.json().catch(()=>({})); typing(false);
|
||||
if(d.answer){ addBubble('assistant',d.answer,d.citations); history[active].push({role:'assistant',content:d.answer,citations:d.citations}); }
|
||||
else addBubble('assistant',d.error||'Something went wrong.',null);
|
||||
} else {
|
||||
typing(false);
|
||||
const live=liveBubble(); let acc=''; let cites=null;
|
||||
const reader=r.body.getReader(); const dec=new TextDecoder(); let buf='';
|
||||
while(true){
|
||||
const {done,value}=await reader.read(); if(done)break;
|
||||
buf+=dec.decode(value,{stream:true});
|
||||
const parts=buf.split('\n\n'); buf=parts.pop();
|
||||
for(const part of parts){
|
||||
const ev=(part.match(/^event: (.+)$/m)||[])[1];
|
||||
const dataLine=(part.match(/^data: (.+)$/m)||[])[1];
|
||||
if(!ev||!dataLine)continue;
|
||||
const payload=JSON.parse(dataLine);
|
||||
if(ev==='delta'){ acc+=payload.t; live.b.textContent=acc; document.getElementById('messages').scrollTop=document.getElementById('messages').scrollHeight; }
|
||||
else if(ev==='done'){ cites=payload.citations; }
|
||||
}
|
||||
}
|
||||
live.m.remove();
|
||||
addBubble('assistant',acc,cites); history[active].push({role:'assistant',content:acc,citations:cites});
|
||||
}
|
||||
}catch(e){ typing(false); addBubble('assistant','Could not reach the engine.',null); }
|
||||
finally{ document.getElementById('sendBtn').disabled=false; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user