fix: clear consumed Telegram challenges after link changes

This commit is contained in:
2026-09-06 14:53:35 +03:00
parent 283bfe5990
commit 37fb7b0366
6 changed files with 43 additions and 12 deletions
+7
View File
@@ -1124,3 +1124,10 @@ Taslak türü seçicisine erişilebilir ad eklendi. Üretim derlemesi geçti. Ö
- Genel veri yazma API'siyle doğrudan Telegram chatId atama yolu kapatıldı. UI sunucudan yeni kod oluşturuyor, son geçerliliği gösteriyor, kod oluşmadan doğrulama sunmuyor. Mevcut bağlı hesaplar korunuyor; otomatik bağlantı-onay mesajı yerine uygulama sonucu gösteriliyor. - Genel veri yazma API'siyle doğrudan Telegram chatId atama yolu kapatıldı. UI sunucudan yeni kod oluşturuyor, son geçerliliği gösteriyor, kod oluşmadan doğrulama sunmuyor. Mevcut bağlı hesaplar korunuyor; otomatik bağlantı-onay mesajı yerine uygulama sonucu gösteriliyor.
- Doğrulama: 266 test ve build başarılı. Gerçek kullanıcı deposunda hesap ayrımı, hash saklama, yenileme, süre sınırı, tüketim ve devam eden doğrulamanın iptali; sahte Telegram yanıtlarında özel/taze/tam eşleşme; HTTP üzerinden doğrudan yazma ve eski kod reddi test edildi. Gerçek Telegram ağına test mesajı gönderilmedi. - Doğrulama: 266 test ve build başarılı. Gerçek kullanıcı deposunda hesap ayrımı, hash saklama, yenileme, süre sınırı, tüketim ve devam eden doğrulamanın iptali; sahte Telegram yanıtlarında özel/taze/tam eşleşme; HTTP üzerinden doğrudan yazma ve eski kod reddi test edildi. Gerçek Telegram ağına test mesajı gönderilmedi.
- Açık kabul: yeni kod ekranının tarayıcı testi, izinli gerçek hesapla bağlama; getUpdates son 100 güncelleme penceresinin yoğun botta yetersiz kalması; genel Telegram bağlantı sürüm/geri dönüş akışları. Genel plan henüz tamamlanmadı. - Açık kabul: yeni kod ekranının tarayıcı testi, izinli gerçek hesapla bağlama; getUpdates son 100 güncelleme penceresinin yoğun botta yetersiz kalması; genel Telegram bağlantı sürüm/geri dönüş akışları. Genel plan henüz tamamlanmadı.
### 2026-09-06 — Telegram kod yenileme ve kaldırma tarayıcı kabulü
- İzole QA sunucusuna gerçek Telegram'a çıkmayan, üretimdeki createTelegramLinker ve geçici kullanıcı deposunu kullanan sağlayıcı taklidi eklendi. Görünür test kontrolleri son kodun sentetik sohbete ulaştığını ve sağlayıcı saatini yönetiyor; üretime dahil edilmiyor.
- Tarayıcı testinde bağlantı kaldırıldıktan sonra tüketilmiş kodun ekranda kaldığı ve Doğrula düğmesinin açık olduğu bulundu. Bağlama/kaldırma tamamlandığında kod artık temizleniyor. Kod yenileme başlarken eski kod kaldırılıyor. Sunucunun geçersiz/süresi dolmuş kod hatası ayrı `TELEGRAM_CHALLENGE_INVALID` koduyla taşınıyor; istemci bu durumda eski kodu temizliyor.
- Dokuz tarayıcı kontrolü geçti: koddan önce doğrulama kapalı, sunucu kodu görünür, süre açıklaması, farklı yeni kod, önceki mesajın reddi, başarılı sentetik bağlama, kaldırmada eski kodun temizlenmesi, doğrulamanın kapanması ve kaldırma onayı. Kanıt `rapor/qa-telegram-link-flow.json`. Test sekmeleri ve bu çalışmanın geçici sunucusu kapatıldı.
- 266 test ve build başarılı; son hata-kodu doğrulaması için üç Telegram birim testi ayrıca geçti. Süre dolması sunucu testinde doğrulandı; süre hatası sonrası kod temizleme kolu bu tarayıcı koşusunda ayrıca tetiklenmedi. Gerçek sağlayıcıda bağlama/gönderim yapılmadı. Genel planın diğer kabul maddeleri açık.
+1 -1
View File
@@ -921,7 +921,7 @@ const server = http.createServer(async (req, res) => {
} }
const bulunan=await linker.link(user.email,body.kod,kodIleBagla); const bulunan=await linker.link(user.email,body.kod,kodIleBagla);
return json(res, 200, { success: true, ad: bulunan.ad }); return json(res, 200, { success: true, ad: bulunan.ad });
} catch (e) { return json(res,e.kod==='VERSION_CONFLICT'?409:e.status||502,{success:false,error:e.message}); } } catch (e) { return json(res,e.kod==='VERSION_CONFLICT'?409:e.status||502,{success:false,error:e.message,kod:e.kod}); }
} }
return json(res,405,{success:false,error:'Desteklenmeyen metot.'}); return json(res,405,{success:false,error:'Desteklenmeyen metot.'});
} }
+1 -1
View File
@@ -5,7 +5,7 @@ export function createTelegramLinker(store,clock=Date.now) {
const snapshot=store.getDataSnapshot(email),value=snapshot.veri.telegram; const snapshot=store.getDataSnapshot(email),value=snapshot.veri.telegram;
if(typeof code!=='string'||!/^[a-f0-9]{48}$/.test(code)||value?.challengeHash!==digest(code)|| if(typeof code!=='string'||!/^[a-f0-9]{48}$/.test(code)||value?.challengeHash!==digest(code)||
!Number.isFinite(value.expiresAt)||clock()>=value.expiresAt) !Number.isFinite(value.expiresAt)||clock()>=value.expiresAt)
throw Object.assign(new Error('Bağlama kodu geçersiz veya süresi dolmuş. Yeni kod oluşturun.'),{status:400}); throw Object.assign(new Error('Bağlama kodu geçersiz veya süresi dolmuş. Yeni kod oluşturun.'),{status:400,kod:'TELEGRAM_CHALLENGE_INVALID'});
return {value,revision:snapshot.revisions.telegram ?? null}; return {value,revision:snapshot.revisions.telegram ?? null};
} }
return { return {
+3 -2
View File
@@ -31,8 +31,8 @@ const TelegramPanel = ({isAdmin}) => {
const calistir = async (ad, fn, basari) => { const calistir = async (ad, fn, basari) => {
if(locked.current)return;locked.current=true; if(locked.current)return;locked.current=true;
setBusy(ad);setMesaj(null); setBusy(ad);setMesaj(null);
try {await fn();if(!alive.current)return;if(ad!=='load')await yenile();if(alive.current&&basari)setMesaj({tur:'ok',metin:basari});} try {await fn();if(!alive.current)return;if(ad==='bagla'||ad==='sil')setChallenge(null);if(ad!=='load')await yenile();if(alive.current&&basari)setMesaj({tur:'ok',metin:basari});}
catch(e){if(alive.current)setMesaj({tur:'hata',metin:e.message});} catch(e){if(alive.current){if(e.kod==='TELEGRAM_CHALLENGE_INVALID')setChallenge(null);setMesaj({tur:'hata',metin:e.message});}}
finally{locked.current=false;if(alive.current)setBusy(null);} finally{locked.current=false;if(alive.current)setBusy(null);}
}; };
useEffect(()=>{alive.current=true;void calistir('load',yenile);return()=>{alive.current=false;};},[]); useEffect(()=>{alive.current=true;void calistir('load',yenile);return()=>{alive.current=false;};},[]);
@@ -95,6 +95,7 @@ const TelegramPanel = ({isAdmin}) => {
<li>Sonra buraya dönüp <strong>Doğrula</strong>'ya basın.</li> <li>Sonra buraya dönüp <strong>Doğrula</strong>'ya basın.</li>
</ol> </ol>
<button disabled={!!busy} className="border rounded px-3 py-2" onClick={()=>calistir('kod',async()=>{ <button disabled={!!busy} className="border rounded px-3 py-2" onClick={()=>calistir('kod',async()=>{
setChallenge(null);
const data=await telegramKodOlustur(); const data=await telegramKodOlustur();
if(typeof data.kod!=='string'||!/^[a-f0-9]{48}$/.test(data.kod)||!Number.isFinite(data.expiresAt))throw new Error('Bağlama kodu doğrulanamadı.'); if(typeof data.kod!=='string'||!/^[a-f0-9]{48}$/.test(data.kod)||!Number.isFinite(data.expiresAt))throw new Error('Bağlama kodu doğrulanamadı.');
if(alive.current)setChallenge(data); if(alive.current)setChallenge(data);
+25 -2
View File
@@ -44,10 +44,33 @@ const qaDelivery=qaSubscriptionStore.reserveDelivery(qaReviewSub.id,{to:user.ema
qaSubscriptionStore.failDelivery(qaDelivery,'Sentetik belirsiz sağlayıcı sonucu.'); qaSubscriptionStore.failDelivery(qaDelivery,'Sentetik belirsiz sağlayıcı sonucu.');
qaSubscriptionStore.reserveDelivery(qaReviewSub.id,null,[]); qaSubscriptionStore.reserveDelivery(qaReviewSub.id,null,[]);
let subscriptionFailure='read'; let subscriptionFailure='read';
let telegramFailure=true; let telegramFailure=true,telegramFixture=false,telegramInbox=null,telegramLatestCode=null,telegramClockOffset=0;
const qaTelegramStore=await import('../server/user-store.mjs');
const {createTelegramLinker}=await import('../server/telegram-link.mjs');
const qaTelegramLinker=createTelegramLinker(qaTelegramStore,()=>Date.now()+telegramClockOffset);
const ui=await createServer({server:{host:'127.0.0.1',port:0,proxy:{'/api':{target:'http://127.0.0.1:'+port}}},plugins:[{name:'qa-fixtures',configureServer(server){server.middlewares.use(async(req,res,next)=>{ const ui=await createServer({server:{host:'127.0.0.1',port:0,proxy:{'/api':{target:'http://127.0.0.1:'+port}}},plugins:[{name:'qa-fixtures',configureServer(server){server.middlewares.use(async(req,res,next)=>{
if(req.url==='/qa-responsive'){res.setHeader('Content-Type','text/html; charset=utf-8');res.end(`<html lang="tr"><h1>Yerel ekran boyutu testi</h1><p>Gerçek uygulama ayrı bir görünüm alanında yüklenir.</p><button data-width="390">390 piksel</button><button data-width="768">768 piksel</button><button data-width="1440">1440 piksel</button><pre id="metrics" aria-label="Görünüm ölçümleri"></pre><iframe title="İhaleist test görünümü" src="/" style="display:block;width:390px;height:900px;border:0"></iframe><script>document.querySelectorAll('button').forEach(button=>button.onclick=()=>document.querySelector('iframe').style.width=button.dataset.width+'px');setInterval(()=>{const doc=document.querySelector('iframe').contentDocument;if(!doc?.body)return;const width=doc.documentElement.clientWidth;const groups=[...doc.querySelectorAll('nav[aria-label="Çalışma alanları"] button')].map(el=>({label:el.textContent.trim(),left:Math.round(el.getBoundingClientRect().left),right:Math.round(el.getBoundingClientRect().right)}));document.getElementById('metrics').textContent=JSON.stringify({width,scrollWidth:doc.documentElement.scrollWidth,groups},null,2);},400);</script></html>`);return;} if(req.url==='/qa-responsive'){res.setHeader('Content-Type','text/html; charset=utf-8');res.end(`<html lang="tr"><h1>Yerel ekran boyutu testi</h1><p>Gerçek uygulama ayrı bir görünüm alanında yüklenir.</p><button data-width="390">390 piksel</button><button data-width="768">768 piksel</button><button data-width="1440">1440 piksel</button><pre id="metrics" aria-label="Görünüm ölçümleri"></pre><iframe title="İhaleist test görünümü" src="/" style="display:block;width:390px;height:900px;border:0"></iframe><script>document.querySelectorAll('button').forEach(button=>button.onclick=()=>document.querySelector('iframe').style.width=button.dataset.width+'px');setInterval(()=>{const doc=document.querySelector('iframe').contentDocument;if(!doc?.body)return;const width=doc.documentElement.clientWidth;const groups=[...doc.querySelectorAll('nav[aria-label="Çalışma alanları"] button')].map(el=>({label:el.textContent.trim(),left:Math.round(el.getBoundingClientRect().left),right:Math.round(el.getBoundingClientRect().right)}));document.getElementById('metrics').textContent=JSON.stringify({width,scrollWidth:doc.documentElement.scrollWidth,groups},null,2);},400);</script></html>`);return;}
if(req.url==='/qa-telegram-controls'){res.setHeader('Content-Type','text/html; charset=utf-8');res.end(`<html lang="tr"><h1>Yerel Telegram testi</h1><button id="recover">Telegram durum hatasını kapat</button><p role="status"></p><script>document.getElementById('recover').onclick=async()=>{await fetch('/qa-telegram-recover',{method:'POST'});document.querySelector('[role=status]').textContent='Hata kapatıldı';};</script></html>`);return;} if(req.url==='/qa-telegram-controls'){res.setHeader('Content-Type','text/html; charset=utf-8');res.end(`<html lang="tr"><h1>Yerel Telegram testi</h1><p>Gerçek Telegram mesajı gönderilmez.</p><button id="recover">Telegram durum hatasını kapat</button><button id="enable">Sentetik bağlamayı aç</button><button id="receive">Son kodu sentetik sohbete ulaştır</button><button id="expire">Kod süresini bitir</button><p role="status"></p><script>for(const mode of ['recover','enable','receive','expire'])document.getElementById(mode).onclick=async()=>{const r=await fetch('/qa-telegram-fixture?mode='+mode,{method:'POST'});document.querySelector('[role=status]').textContent=await r.text();};</script></html>`);return;}
if(req.url?.startsWith('/qa-telegram-fixture?')&&req.method==='POST'){
const mode=new URL(req.url,'http://localhost').searchParams.get('mode');
if(mode==='recover')telegramFailure=false;
if(mode==='enable'){telegramFixture=true;telegramFailure=false;}
if(mode==='receive')telegramInbox=telegramLatestCode;
if(mode==='expire')telegramClockOffset+=11*60000;
res.end('Sentetik Telegram durumu: '+mode);return;
}
if(req.url==='/api/me/telegram'&&telegramFixture&&['GET','POST'].includes(req.method)){
const reply=(status,data)=>{res.writeHead(status,{'Content-Type':'application/json'});res.end(JSON.stringify(data));};
if(req.headers.authorization!=='Bearer '+token){reply(401,{success:false,error:'Test hesabı gerekli.'});return;}
try {
if(req.method==='GET'){const tg=qaTelegramStore.getData(user.email,'telegram');reply(200,{success:true,yapilandirilmis:true,bagli:!!tg?.chatId,bot:null,ad:tg?.ad||null});return;}
let raw='';for await(const chunk of req)raw+=chunk;const body=JSON.parse(raw);
if(body.islem==='kod'){const challenge=qaTelegramLinker.issue(user.email);telegramLatestCode=challenge.kod;reply(200,{success:true,...challenge});return;}
if(body.islem==='test'){reply(400,{success:false,error:'Bu test gerçek mesaj göndermez.'});return;}
const found=await qaTelegramLinker.link(user.email,body.kod,async code=>code===telegramInbox?{chatId:'123456789',ad:'QA Sentetik Sohbet'}:null);
reply(200,{success:true,ad:found.ad});
}catch(error){reply(error.status||400,{success:false,error:error.message,kod:error.kod});}return;
}
if(req.url==='/qa-telegram-recover'&&req.method==='POST'){telegramFailure=false;res.end('ok');return;} if(req.url==='/qa-telegram-recover'&&req.method==='POST'){telegramFailure=false;res.end('ok');return;}
if(req.url==='/api/me/telegram'&&req.method==='GET'&&telegramFailure){res.writeHead(503,{'Content-Type':'application/json'});res.end(JSON.stringify({success:false,error:'QA: Telegram durumu alınamadı.'}));return;} if(req.url==='/api/me/telegram'&&req.method==='GET'&&telegramFailure){res.writeHead(503,{'Content-Type':'application/json'});res.end(JSON.stringify({success:false,error:'QA: Telegram durumu alınamadı.'}));return;}
if(req.url==='/qa-subscription-controls'){res.setHeader('Content-Type','text/html; charset=utf-8');res.end(`<html lang="tr"><h1>Yerel abonelik testi</h1><button id="read">Abonelik liste hatası</button><button id="delete">Abonelik silme hatası</button><button id="none">Abonelik hatalarını kapat</button><p role="status"></p><script>for(const mode of ['read','delete','none'])document.getElementById(mode).onclick=async()=>{const r=await fetch('/qa-subscription-failure?mode='+mode,{method:'POST'});document.querySelector('[role=status]').textContent=await r.text();};</script></html>`);return;} if(req.url==='/qa-subscription-controls'){res.setHeader('Content-Type','text/html; charset=utf-8');res.end(`<html lang="tr"><h1>Yerel abonelik testi</h1><button id="read">Abonelik liste hatası</button><button id="delete">Abonelik silme hatası</button><button id="none">Abonelik hatalarını kapat</button><p role="status"></p><script>for(const mode of ['read','delete','none'])document.getElementById(mode).onclick=async()=>{const r=await fetch('/qa-subscription-failure?mode='+mode,{method:'POST'});document.querySelector('[role=status]').textContent=await r.text();};</script></html>`);return;}
+6 -6
View File
@@ -15,14 +15,14 @@ test('server challenge belongs to its account, expires, rotates and is consumed
let time=1000000;const linker=createTelegramLinker(store,()=>time),email='link@example.test'; let time=1000000;const linker=createTelegramLinker(store,()=>time),email='link@example.test';
const first=linker.issue(email);assert.match(first.kod,/^[a-f0-9]{48}$/); const first=linker.issue(email);assert.match(first.kod,/^[a-f0-9]{48}$/);
assert.ok(!JSON.stringify(store.getData(email,'telegram')).includes(first.kod)); assert.ok(!JSON.stringify(store.getData(email,'telegram')).includes(first.kod));
await assert.rejects(linker.link('other@example.test',first.kod,async()=>found),{status:400}); await assert.rejects(linker.link('other@example.test',first.kod,async()=>found),{status:400,kod:'TELEGRAM_CHALLENGE_INVALID'});
const second=linker.issue(email);assert.notEqual(second.kod,first.kod); const second=linker.issue(email);assert.notEqual(second.kod,first.kod);
await assert.rejects(linker.link(email,first.kod,async()=>found),{status:400}); await assert.rejects(linker.link(email,first.kod,async()=>found),{status:400,kod:'TELEGRAM_CHALLENGE_INVALID'});
time=second.expiresAt; time=second.expiresAt;
await assert.rejects(linker.link(email,second.kod,async()=>found),{status:400}); await assert.rejects(linker.link(email,second.kod,async()=>found),{status:400,kod:'TELEGRAM_CHALLENGE_INVALID'});
const current=linker.issue(email); const current=linker.issue(email);
assert.deepEqual(await linker.link(email,current.kod,async()=>found),found); assert.deepEqual(await linker.link(email,current.kod,async()=>found),found);
await assert.rejects(linker.link(email,current.kod,async()=>found),{status:400}); await assert.rejects(linker.link(email,current.kod,async()=>found),{status:400,kod:'TELEGRAM_CHALLENGE_INVALID'});
assert.equal(store.getData(email,'telegram').chatId,'123'); assert.equal(store.getData(email,'telegram').chatId,'123');
assert.throws(()=>linker.issue(email),{status:409}); assert.throws(()=>linker.issue(email),{status:409});
}); });
@@ -30,10 +30,10 @@ test('server challenge belongs to its account, expires, rotates and is consumed
test('disconnect or expiration during provider lookup cannot restore a removed link',async()=>{ test('disconnect or expiration during provider lookup cannot restore a removed link',async()=>{
let time=1000000;const linker=createTelegramLinker(store,()=>time),email='disconnect@example.test'; let time=1000000;const linker=createTelegramLinker(store,()=>time),email='disconnect@example.test';
const challenge=linker.issue(email); const challenge=linker.issue(email);
await assert.rejects(linker.link(email,challenge.kod,async()=>{store.setData(email,'telegram',null);return found;}),{status:400}); await assert.rejects(linker.link(email,challenge.kod,async()=>{store.setData(email,'telegram',null);return found;}),{status:400,kod:'TELEGRAM_CHALLENGE_INVALID'});
assert.equal(store.getData(email,'telegram'),null); assert.equal(store.getData(email,'telegram'),null);
const next=linker.issue(email); const next=linker.issue(email);
await assert.rejects(linker.link(email,next.kod,async()=>{time=next.expiresAt;return found;}),{status:400}); await assert.rejects(linker.link(email,next.kod,async()=>{time=next.expiresAt;return found;}),{status:400,kod:'TELEGRAM_CHALLENGE_INVALID'});
}); });
test('matching accepts only a fresh exact code from a private non-bot sender',async()=>{ test('matching accepts only a fresh exact code from a private non-bot sender',async()=>{