fix: bind Telegram pairing to expiring single-use account challenges
This commit is contained in:
@@ -1116,3 +1116,11 @@ Taslak türü seçicisine erişilebilir ad eklendi. Üretim derlemesi geçti. Ö
|
|||||||
- Test sırasında kurum tipi seçiminin erişilebilir adının eksik olduğu bulundu; `Filtre kurum tipi` adı eklendi. Diğer seçimlerde mevcut erişilebilir adlar kullanıldı. QA verisine sentetik usul/kurum tipi alanları eklendi; resmi ihale verisi olarak değerlendirilmez.
|
- Test sırasında kurum tipi seçiminin erişilebilir adının eksik olduğu bulundu; `Filtre kurum tipi` adı eklendi. Diğer seçimlerde mevcut erişilebilir adlar kullanıldı. QA verisine sentetik usul/kurum tipi alanları eklendi; resmi ihale verisi olarak değerlendirilmez.
|
||||||
- Beş tarayıcı kontrolü geçti: ilk birleşik sonuç, yenileme sonrası yeniden uygulama, çelişen bölge boş sonucu, sıfırlama ve tekrar uygulama. Kanıt `rapor/qa-combined-saved-filter.json`. Build başarılı. Test sekmesi ve bu çalışmaya ait geçici sunucu kapatıldı. Bu dar UI değişikliği için otomatik süit tekrar çalıştırılmadı; son süit 262 testte geçmişti.
|
- Beş tarayıcı kontrolü geçti: ilk birleşik sonuç, yenileme sonrası yeniden uygulama, çelişen bölge boş sonucu, sıfırlama ve tekrar uygulama. Kanıt `rapor/qa-combined-saved-filter.json`. Build başarılı. Test sekmesi ve bu çalışmaya ait geçici sunucu kapatıldı. Bu dar UI değişikliği için otomatik süit tekrar çalıştırılmadı; son süit 262 testte geçmişti.
|
||||||
- D05/D09 birleşik filtre kullanıcı akışına kanıt eklendi. Resmi kaynak alanlarının doğruluğu, gerçek firma ile beş başvuru dosyası ve diğer kabul maddeleri hâlâ açık; genel plan tamamlanmadı.
|
- D05/D09 birleşik filtre kullanıcı akışına kanıt eklendi. Resmi kaynak alanlarının doğruluğu, gerçek firma ile beş başvuru dosyası ve diğer kabul maddeleri hâlâ açık; genel plan tamamlanmadı.
|
||||||
|
|
||||||
|
### 2026-09-06 — Hesaba bağlı, süreli Telegram doğrulaması
|
||||||
|
|
||||||
|
- Güvenlik/işlev hatası: eski kod tarayıcıda Math.random ile üretiliyor, herhangi bir altı haneli değer kabul ediliyor ve getUpdates mesajının rakamları içinde alt-dizi aranıyordu; süre/hesap/tek kullanım yoktu. Sunucuda 24 rastgele baytlık kod, yalnız hash saklama, 10 dakika süre, hesaba bağlı doğrulama ve tüketim eklendi.
|
||||||
|
- Özel sohbet, insan gönderici ve sohbet/gönderici kimliği eşleşmesi gerekiyor. Yalnız yeni mesajın tamamının kod veya `/start kod` olması kabul ediliyor; grup, bot, eski/gelecek tarih, düzenlenmiş mesaj ve alt-dizi eşleşmesi reddediliyor. Sağlayıcı okumasından sonra kod tekrar doğrulanıyor; bu sırada iptal/yenileme/süre dolması bağlantıyı engelliyor. Sürüm kontrollü kayıt tek kullanımı koruyor.
|
||||||
|
- 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.
|
||||||
|
- 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ı.
|
||||||
|
|||||||
@@ -93,3 +93,5 @@ Eski `server/data/brief-son.json` silinmez: eski sürümün aynı hafta için i
|
|||||||
Haftalık belirsiz kayıtların karar akışı artık Yönetim → Mobil PWA → **Haftalık özet gönderim incelemesi** bölümündedir. `/api/admin/brief/outbox` yalnız yöneticiye açıktır. Gönderildi kararı sağlayıcı kimliği, açık kontrol onayı ve 20–2000 karakterlik açıklama ister; doğrulanamayan mesaj yeni gönderim yapmadan kapatılabilir. `brief_reviews` tablosu karar sahibini, türünü, açıklamasını ve tarihini aynı transaction içinde saklar. İkinci karar 409 ile reddedilir. Süresi dolmuş gönderim sahipliği liste yenilendiğinde belirsiz olarak görünür; etkin gönderim için karar verilemez. Tam volume yedeği karar tablosunu da kapsar. Bu bölüm önceki nottaki eksik yönetici karar ekranını tamamlar; sağlayıcı teslimat kanıtını otomatik olarak doğrulamaz.
|
Haftalık belirsiz kayıtların karar akışı artık Yönetim → Mobil PWA → **Haftalık özet gönderim incelemesi** bölümündedir. `/api/admin/brief/outbox` yalnız yöneticiye açıktır. Gönderildi kararı sağlayıcı kimliği, açık kontrol onayı ve 20–2000 karakterlik açıklama ister; doğrulanamayan mesaj yeni gönderim yapmadan kapatılabilir. `brief_reviews` tablosu karar sahibini, türünü, açıklamasını ve tarihini aynı transaction içinde saklar. İkinci karar 409 ile reddedilir. Süresi dolmuş gönderim sahipliği liste yenilendiğinde belirsiz olarak görünür; etkin gönderim için karar verilemez. Tam volume yedeği karar tablosunu da kapsar. Bu bölüm önceki nottaki eksik yönetici karar ekranını tamamlar; sağlayıcı teslimat kanıtını otomatik olarak doğrulamaz.
|
||||||
|
|
||||||
Yeni haftalık Telegram kabulleri `brief_messages.receipt` alanında `chat_id:message_id` olarak saklanır. Eski `telegram-accepted` değerleri gerçek mesaj kimliği değildir ve geriye dönük kimlik üretilmez. Sağlayıcı incelemesinde mesajın ait olduğu sohbet de kontrol edilmelidir; mesaj numarası tek başına bütün sohbetlerde tekil kabul edilmez.
|
Yeni haftalık Telegram kabulleri `brief_messages.receipt` alanında `chat_id:message_id` olarak saklanır. Eski `telegram-accepted` değerleri gerçek mesaj kimliği değildir ve geriye dönük kimlik üretilmez. Sağlayıcı incelemesinde mesajın ait olduğu sohbet de kontrol edilmelidir; mesaj numarası tek başına bütün sohbetlerde tekil kabul edilmez.
|
||||||
|
|
||||||
|
Telegram bağlama protokolü değişti: tarayıcının ürettiği altı haneli kod kabul edilmez. Oturumlu POST `/api/me/telegram` `{islem:"kod"}` sunucuda 24 rastgele baytlık kod üretir; kullanıcı deposunda yalnız hash, oluşturulma ve 10 dakikalık son geçerlilik saklanır. Yeni kod önceki kodu geçersiz kılar. Doğrulama yalnız hesapta aktif olan kodu tüketir; bağlantı kaldırma veya kod yenileme sürmekteki doğrulamayı da geçersiz kılar. Ön yüz ve sunucu birlikte güncellenmelidir. Mevcut bağlı sohbetler korunur. Genel `/api/me/data` yolu Telegram verisini yazamaz; bağlantı ekranı kullanılmalıdır. Bağlama sonrası otomatik Telegram onay mesajı kaldırıldı; bağlantı sonucu uygulamada gösterilir. Ayrı test mesajı düğmesi yine açık kullanıcı eylemidir.
|
||||||
|
|||||||
+11
-8
@@ -39,6 +39,7 @@ import { kalemleriCikar } from './kalem-cikar.mjs';
|
|||||||
import { searchInDocuments, searchByOkas, okasAra } from './ekap-v2.mjs';
|
import { searchInDocuments, searchByOkas, okasAra } from './ekap-v2.mjs';
|
||||||
import { iknCikar } from './kalem-cikar.mjs';
|
import { iknCikar } from './kalem-cikar.mjs';
|
||||||
import { bonusHesapla, gommeDurumu, arkaPlandaIsit } from './uyum-gomme.mjs';
|
import { bonusHesapla, gommeDurumu, arkaPlandaIsit } from './uyum-gomme.mjs';
|
||||||
|
import { createTelegramLinker } from './telegram-link.mjs';
|
||||||
import { telegramConfigured, botAdi, kodIleBagla, sendTelegram } from './telegram.mjs';
|
import { telegramConfigured, botAdi, kodIleBagla, sendTelegram } from './telegram.mjs';
|
||||||
import { bildirimleriOku, okunduIsaretle, zeyilnameDurumu, zeyilnameTara, sonucTara } from './zeyilname.mjs';
|
import { bildirimleriOku, okunduIsaretle, zeyilnameDurumu, zeyilnameTara, sonucTara } from './zeyilname.mjs';
|
||||||
|
|
||||||
@@ -897,7 +898,7 @@ const server = http.createServer(async (req, res) => {
|
|||||||
return json(res, 200, { success: true, ...gommeDurumu() });
|
return json(res, 200, { success: true, ...gommeDurumu() });
|
||||||
}
|
}
|
||||||
// --- Telegram bağlantısı (kullanıcı kodu bota yazar, biz getUpdates'te görürüz) ---
|
// --- Telegram bağlantısı (kullanıcı kodu bota yazar, biz getUpdates'te görürüz) ---
|
||||||
if (url.pathname.startsWith('/api/me/telegram')) {
|
if (url.pathname === '/api/me/telegram') {
|
||||||
const user = userFromAuthHeader(req.headers['authorization']);
|
const user = userFromAuthHeader(req.headers['authorization']);
|
||||||
if (!user) return json(res, 401, { success: false, error: 'oturum yok' });
|
if (!user) return json(res, 401, { success: false, error: 'oturum yok' });
|
||||||
const tg = store.getData(user.email, 'telegram');
|
const tg = store.getData(user.email, 'telegram');
|
||||||
@@ -913,15 +914,16 @@ const server = http.createServer(async (req, res) => {
|
|||||||
await sendTelegram(tg.chatId, '✅ İhaleist bağlantı testi — bildirimler bu sohbete gelecek.');
|
await sendTelegram(tg.chatId, '✅ İhaleist bağlantı testi — bildirimler bu sohbete gelecek.');
|
||||||
return json(res, 200, { success: true });
|
return json(res, 200, { success: true });
|
||||||
}
|
}
|
||||||
const kod = String(body.kod || '').replace(/\D/g, '');
|
const linker=createTelegramLinker(store);
|
||||||
if (kod.length !== 6) return json(res, 400, { success: false, error: '6 haneli kod gerekli' });
|
if(body.islem==='kod'){
|
||||||
const bulunan = await kodIleBagla(kod);
|
if(!telegramConfigured())return json(res,503,{success:false,error:'Telegram hizmeti etkin değil.'});
|
||||||
if (!bulunan) return json(res, 404, { success: false, error: 'Kod bota gönderilmiş görünmüyor. Önce bota yazın, sonra doğrulayın.' });
|
return json(res,200,{success:true,...linker.issue(user.email)});
|
||||||
store.setData(user.email, 'telegram', { ...bulunan, baglanma: new Date().toISOString() });
|
}
|
||||||
await sendTelegram(bulunan.chatId, `✅ İhaleist hesabınız (${user.email}) bu sohbete bağlandı.`).catch(() => {});
|
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, 502, { success: false, error: e.message }); }
|
} catch (e) { return json(res,e.kod==='VERSION_CONFLICT'?409:e.status||502,{success:false,error:e.message}); }
|
||||||
}
|
}
|
||||||
|
return json(res,405,{success:false,error:'Desteklenmeyen metot.'});
|
||||||
}
|
}
|
||||||
// --- Takip bildirimleri (zeyilname / sonuç) ---
|
// --- Takip bildirimleri (zeyilname / sonuç) ---
|
||||||
if (url.pathname === '/api/me/bildirimler') {
|
if (url.pathname === '/api/me/bildirimler') {
|
||||||
@@ -1217,6 +1219,7 @@ const server = http.createServer(async (req, res) => {
|
|||||||
const protectedKey=['firma_profil','saved_filters','ekip','favori_kurumlar','rakip_izleme','reminder','brief'].includes(k);
|
const protectedKey=['firma_profil','saved_filters','ekip','favori_kurumlar','rakip_izleme','reminder','brief'].includes(k);
|
||||||
if(protectedKey && (!Object.hasOwn(body,'revision') || (body.revision!==null && (typeof body.revision!=='string' || !/^[a-f0-9]{64}$/.test(body.revision)))))
|
if(protectedKey && (!Object.hasOwn(body,'revision') || (body.revision!==null && (typeof body.revision!=='string' || !/^[a-f0-9]{64}$/.test(body.revision)))))
|
||||||
return json(res,428,{success:false,kod:'VERSION_REQUIRED',error:'Güncel kayıt sürümü gerekli. Sayfayı yenileyip tekrar deneyin.'});
|
return json(res,428,{success:false,kod:'VERSION_REQUIRED',error:'Güncel kayıt sürümü gerekli. Sayfayı yenileyip tekrar deneyin.'});
|
||||||
|
if(k==='telegram')return json(res,403,{success:false,error:'Telegram bağlantısını doğrulama ekranından yönetin.'});
|
||||||
const revision=store.setData(user.email, k, body.deger, protectedKey ? body.revision : undefined);
|
const revision=store.setData(user.email, k, body.deger, protectedKey ? body.revision : undefined);
|
||||||
return json(res, 200, { success: true, ...(protectedKey ? {revision} : {}) });
|
return json(res, 200, { success: true, ...(protectedKey ? {revision} : {}) });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { randomBytes, createHash } from 'node:crypto';
|
||||||
|
const digest = code => createHash('sha256').update(code).digest('hex');
|
||||||
|
export function createTelegramLinker(store,clock=Date.now) {
|
||||||
|
function validate(email,code) {
|
||||||
|
const snapshot=store.getDataSnapshot(email),value=snapshot.veri.telegram;
|
||||||
|
if(typeof code!=='string'||!/^[a-f0-9]{48}$/.test(code)||value?.challengeHash!==digest(code)||
|
||||||
|
!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});
|
||||||
|
return {value,revision:snapshot.revisions.telegram ?? null};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
issue(email) {
|
||||||
|
const snapshot=store.getDataSnapshot(email);
|
||||||
|
if(snapshot.veri.telegram?.chatId)throw Object.assign(new Error('Önce mevcut Telegram bağlantısını kaldırın.'),{status:409});
|
||||||
|
const code=randomBytes(24).toString('hex'),issuedAt=clock(),expiresAt=issuedAt+10*60000;
|
||||||
|
store.setData(email,'telegram',{challengeHash:digest(code),issuedAt,expiresAt},snapshot.revisions.telegram ?? null);
|
||||||
|
return {kod:code,expiresAt};
|
||||||
|
},
|
||||||
|
async link(email,code,find) {
|
||||||
|
const {value}=validate(email,code);
|
||||||
|
const found=await find(code,{issuedAt:value.issuedAt,expiresAt:value.expiresAt,now:clock()});
|
||||||
|
if(!found)throw Object.assign(new Error('Kod özel sohbette bulunamadı. Yeni kodu bota gönderip tekrar doğrulayın.'),{status:404});
|
||||||
|
const current=validate(email,code);
|
||||||
|
store.setData(email,'telegram',{chatId:found.chatId,ad:found.ad,baglanma:new Date(clock()).toISOString()},current.revision);
|
||||||
|
return found;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
+11
-9
@@ -2,7 +2,7 @@
|
|||||||
* Telegram bildirim kanalı — Bot API, bağımlılık yok.
|
* Telegram bildirim kanalı — Bot API, bağımlılık yok.
|
||||||
*
|
*
|
||||||
* KURULUM (kullanıcı/yönetici): BotFather'da bot oluşturulur, tokenı
|
* KURULUM (kullanıcı/yönetici): BotFather'da bot oluşturulur, tokenı
|
||||||
* TELEGRAM_BOT_TOKEN env'ine girilir. Her kullanıcı uygulamadan aldığı 6 haneli
|
* TELEGRAM_BOT_TOKEN env'ine girilir. Her kullanıcı uygulamadan aldığı süreli
|
||||||
* kodu bota yazar; sunucu getUpdates ile kodu görüp chat id'yi kullanıcıya
|
* kodu bota yazar; sunucu getUpdates ile kodu görüp chat id'yi kullanıcıya
|
||||||
* bağlar. Token bizde tutulmaz, sohbet kimliği kullanıcı verisinde durur.
|
* bağlar. Token bizde tutulmaz, sohbet kimliği kullanıcı verisinde durur.
|
||||||
*/
|
*/
|
||||||
@@ -48,22 +48,24 @@ export async function sendTelegram(chatId, text, { parseMode = 'HTML', returnRec
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kullanıcının bota yazdığı 6 haneli kodu son güncellemelerde arar.
|
* Kullanıcının bota yazdığı süreli bağlama kodunu son güncellemelerde arar.
|
||||||
* Bulursa { chatId, ad } döner. Long-polling yok; kullanıcı "doğrula" dediğinde
|
* Bulursa { chatId, ad } döner. Long-polling yok; kullanıcı "doğrula" dediğinde
|
||||||
* tek getUpdates çağrısı yeterli (Telegram son 24 saati tutar).
|
* tek getUpdates çağrısı yeterli (Telegram son 24 saati tutar).
|
||||||
*/
|
*/
|
||||||
export async function kodIleBagla(kod) {
|
export async function kodIleBagla(kod, {issuedAt,expiresAt,now=Date.now()} = {}) {
|
||||||
if (!telegramConfigured()) throw new Error('Sunucuda TELEGRAM_BOT_TOKEN tanımlı değil');
|
if (!telegramConfigured()) throw new Error('Sunucuda TELEGRAM_BOT_TOKEN tanımlı değil');
|
||||||
|
if(!/^[a-f0-9]{48}$/.test(String(kod))||!Number.isFinite(issuedAt)||!Number.isFinite(expiresAt)||now>=expiresAt)return null;
|
||||||
const r = await fetch(`${API()}/getUpdates?limit=100`, { signal: AbortSignal.timeout(15000) });
|
const r = await fetch(`${API()}/getUpdates?limit=100`, { signal: AbortSignal.timeout(15000) });
|
||||||
const d = await r.json().catch(() => ({}));
|
const d = await r.json().catch(() => ({}));
|
||||||
if (!d.ok) throw new Error(d.description || 'getUpdates başarısız');
|
if (!d.ok) throw new Error(d.description || 'getUpdates başarısız');
|
||||||
const aranan = String(kod).trim();
|
const aranan = String(kod);
|
||||||
for (const u of [...(d.result || [])].reverse()) {
|
for (const u of [...(d.result || [])].reverse()) {
|
||||||
const m = u.message || u.edited_message;
|
const m = u.message;
|
||||||
if (!m?.text) continue;
|
if(typeof m?.text!=='string'||m.chat?.type!=='private'||m.from?.is_bot!==false||
|
||||||
if (m.text.replace(/\D/g, '').includes(aranan)) {
|
!Number.isSafeInteger(m.chat.id)||m.chat.id<=0||m.from.id!==m.chat.id||
|
||||||
return { chatId: String(m.chat.id), ad: [m.chat.first_name, m.chat.last_name].filter(Boolean).join(' ') || m.chat.username || null };
|
!Number.isFinite(m.date)||m.date<Math.floor(issuedAt/1000)||m.date>Math.floor(now/1000))continue;
|
||||||
}
|
if(m.text.trim()===aranan || m.text.trim()===`/start ${aranan}`)
|
||||||
|
return {chatId:String(m.chat.id),ad:[m.chat.first_name,m.chat.last_name].filter(Boolean).join(' ')||m.chat.username||null};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import {useApp} from '../../context/AppContext';
|
|||||||
import {getToken} from '../../services/auth';
|
import {getToken} from '../../services/auth';
|
||||||
import React, { useEffect, useState, useRef } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
import { Send, CheckCircle2, AlertTriangle, Loader2, Unplug } from 'lucide-react';
|
import { Send, CheckCircle2, AlertTriangle, Loader2, Unplug } from 'lucide-react';
|
||||||
import { telegramDurum, telegramBagla, telegramTest, telegramSil } from '../../services/account';
|
import { telegramDurum, telegramKodOlustur, telegramBagla, telegramTest, telegramSil } from '../../services/account';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Telegram bildirim bağlantısı — kullanıcı kendi kurar.
|
* Telegram bildirim bağlantısı — kullanıcı kendi kurar.
|
||||||
* Bot tokenı sunucuda (TELEGRAM_BOT_TOKEN); kullanıcı 6 haneli kodu bota yazar,
|
* Bot tokenı sunucuda (TELEGRAM_BOT_TOKEN); kullanıcı süreli bağlama kodunu bota yazar,
|
||||||
* "Doğrula" deyince sunucu kodu görüp sohbeti hesaba bağlar. Token bizden geçmez.
|
* "Doğrula" deyince sunucu kodu görüp sohbeti hesaba bağlar. Token bizden geçmez.
|
||||||
*/
|
*/
|
||||||
const kodUret = () => String(Math.floor(100000 + Math.random() * 900000));
|
|
||||||
|
|
||||||
export const TelegramBaglanti = () => {
|
export const TelegramBaglanti = () => {
|
||||||
const {authUser,isAdmin}=useApp();
|
const {authUser,isAdmin}=useApp();
|
||||||
@@ -17,7 +17,8 @@ export const TelegramBaglanti = () => {
|
|||||||
};
|
};
|
||||||
const TelegramPanel = ({isAdmin}) => {
|
const TelegramPanel = ({isAdmin}) => {
|
||||||
const [d, setD] = useState(null);
|
const [d, setD] = useState(null);
|
||||||
const [kod] = useState(kodUret);
|
const [challenge,setChallenge]=useState(null);
|
||||||
|
const kod=challenge?.kod;
|
||||||
const [busy, setBusy] = useState(null);
|
const [busy, setBusy] = useState(null);
|
||||||
const [mesaj, setMesaj] = useState(null);
|
const [mesaj, setMesaj] = useState(null);
|
||||||
|
|
||||||
@@ -90,10 +91,16 @@ const TelegramPanel = ({isAdmin}) => {
|
|||||||
: <span className="font-bold">(bot adı alınamadı — yöneticiye sorun)</span>}
|
: <span className="font-bold">(bot adı alınamadı — yöneticiye sorun)</span>}
|
||||||
{' '}ve <strong>Başlat</strong>'a basın.
|
{' '}ve <strong>Başlat</strong>'a basın.
|
||||||
</li>
|
</li>
|
||||||
<li>Bota şu kodu yazıp gönderin: <code className="bg-white border border-sky-300 px-2 py-0.5 rounded font-mono text-sm font-black tracking-widest">{kod}</code></li>
|
<li>Yeni kod oluşturun ve bota kodun tamamını gönderin: <code className="bg-white border border-sky-300 px-2 py-0.5 rounded font-mono text-sm font-black break-all">{kod || 'Henüz kod oluşturulmadı'}</code></li>
|
||||||
<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 onClick={() => calistir('bagla', () => telegramBagla(kod), 'Telegram bağlantısı kaydedildi.')} disabled={!!busy}
|
<button disabled={!!busy} className="border rounded px-3 py-2" onClick={()=>calistir('kod',async()=>{
|
||||||
|
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(alive.current)setChallenge(data);
|
||||||
|
})}>Yeni bağlama kodu oluştur</button>
|
||||||
|
{challenge&&<p>Kod 10 dakika geçerlidir. Son geçerlilik: {new Date(challenge.expiresAt).toLocaleTimeString('tr-TR')}. Yeni kod oluşturmak önceki kodu geçersiz kılar.</p>}
|
||||||
|
<button onClick={() => calistir('bagla', () => telegramBagla(kod), 'Telegram bağlantısı kaydedildi.')} disabled={!!busy || !kod}
|
||||||
className="bg-sky-600 hover:bg-sky-500 disabled:opacity-40 text-white font-bold text-xs px-5 py-2 rounded-lg flex items-center gap-1.5">
|
className="bg-sky-600 hover:bg-sky-500 disabled:opacity-40 text-white font-bold text-xs px-5 py-2 rounded-lg flex items-center gap-1.5">
|
||||||
{busy === 'bagla' ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <CheckCircle2 className="w-3.5 h-3.5" />} Doğrula
|
{busy === 'bagla' ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <CheckCircle2 className="w-3.5 h-3.5" />} Doğrula
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ export const updateDocumentMeta = (id, { sonKullanma, kategori } = {}) =>
|
|||||||
|
|
||||||
// --- Telegram bağlantısı ---
|
// --- Telegram bağlantısı ---
|
||||||
export const telegramDurum = () => iste('/api/me/telegram');
|
export const telegramDurum = () => iste('/api/me/telegram');
|
||||||
|
export const telegramKodOlustur = () => iste('/api/me/telegram', {method:'POST',body:JSON.stringify({islem:'kod'})});
|
||||||
export const telegramBagla = (kod) => iste('/api/me/telegram', { method: 'POST', body: JSON.stringify({ kod }) });
|
export const telegramBagla = (kod) => iste('/api/me/telegram', { method: 'POST', body: JSON.stringify({ kod }) });
|
||||||
export const telegramTest = () => iste('/api/me/telegram', { method: 'POST', body: JSON.stringify({ islem: 'test' }) });
|
export const telegramTest = () => iste('/api/me/telegram', { method: 'POST', body: JSON.stringify({ islem: 'test' }) });
|
||||||
export const telegramSil = () => iste('/api/me/telegram', { method: 'DELETE' });
|
export const telegramSil = () => iste('/api/me/telegram', { method: 'DELETE' });
|
||||||
|
|||||||
@@ -287,3 +287,10 @@ test('weekly brief review is admin-only and persists evidence without sending me
|
|||||||
assert.equal(saved.review.actor,'weekly-admin@example.test');assert.equal(saved.providerId,input.providerId);
|
assert.equal(saved.review.actor,'weekly-admin@example.test');assert.equal(saved.providerId,input.providerId);
|
||||||
assert.equal(saved.state,'sent_manual');
|
assert.equal(saved.state,'sent_manual');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Telegram cannot be linked through generic data writes or legacy client codes',async()=>{
|
||||||
|
assert.equal((await request('/api/me/data','PUT',{anahtar:'telegram',deger:{chatId:'123'}})).status,403);
|
||||||
|
assert.equal((await request('/api/me/telegram','POST',{kod:'123456'})).status,400);
|
||||||
|
assert.equal((await request('/api/me/telegram','PATCH',{})).status,405);
|
||||||
|
assert.equal((await request('/api/me/telegram','POST',{islem:'kod'},null)).status,401);
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import {test,after} from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {mkdtempSync,rmSync} from 'node:fs';
|
||||||
|
import {join} from 'node:path';
|
||||||
|
import {tmpdir} from 'node:os';
|
||||||
|
import {createTelegramLinker} from '../server/telegram-link.mjs';
|
||||||
|
import {kodIleBagla} from '../server/telegram.mjs';
|
||||||
|
const directory=mkdtempSync(join(tmpdir(),'telegram-link-'));
|
||||||
|
process.env.IHALEIST_USER_DATA_DIR=directory;
|
||||||
|
const store=await import('../server/user-store.mjs');
|
||||||
|
after(()=>rmSync(directory,{recursive:true,force:true}));
|
||||||
|
const found={chatId:'123',ad:'Test'};
|
||||||
|
|
||||||
|
test('server challenge belongs to its account, expires, rotates and is consumed once',async()=>{
|
||||||
|
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}$/);
|
||||||
|
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});
|
||||||
|
const second=linker.issue(email);assert.notEqual(second.kod,first.kod);
|
||||||
|
await assert.rejects(linker.link(email,first.kod,async()=>found),{status:400});
|
||||||
|
time=second.expiresAt;
|
||||||
|
await assert.rejects(linker.link(email,second.kod,async()=>found),{status:400});
|
||||||
|
const current=linker.issue(email);
|
||||||
|
assert.deepEqual(await linker.link(email,current.kod,async()=>found),found);
|
||||||
|
await assert.rejects(linker.link(email,current.kod,async()=>found),{status:400});
|
||||||
|
assert.equal(store.getData(email,'telegram').chatId,'123');
|
||||||
|
assert.throws(()=>linker.issue(email),{status:409});
|
||||||
|
});
|
||||||
|
|
||||||
|
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';
|
||||||
|
const challenge=linker.issue(email);
|
||||||
|
await assert.rejects(linker.link(email,challenge.kod,async()=>{store.setData(email,'telegram',null);return found;}),{status:400});
|
||||||
|
assert.equal(store.getData(email,'telegram'),null);
|
||||||
|
const next=linker.issue(email);
|
||||||
|
await assert.rejects(linker.link(email,next.kod,async()=>{time=next.expiresAt;return found;}),{status:400});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('matching accepts only a fresh exact code from a private non-bot sender',async()=>{
|
||||||
|
const oldFetch=globalThis.fetch,oldToken=process.env.TELEGRAM_BOT_TOKEN;
|
||||||
|
process.env.TELEGRAM_BOT_TOKEN='synthetic-token';
|
||||||
|
const code='a'.repeat(48),window={issuedAt:1000000,expiresAt:1600000,now:1100000};
|
||||||
|
const good={text:code,date:1050,chat:{id:123,type:'private',first_name:'QA'},from:{id:123,is_bot:false}};
|
||||||
|
let updates=[];
|
||||||
|
globalThis.fetch=async()=>({ok:true,json:async()=>({ok:true,result:updates})});
|
||||||
|
try {
|
||||||
|
for(const message of [{...good,text:'prefix '+code},{...good,text:code+' suffix'},{...good,date:999},
|
||||||
|
{...good,date:1101},{...good,chat:{id:123,type:'group'}},{...good,from:{id:123,is_bot:true}},
|
||||||
|
{...good,from:{id:999,is_bot:false}}]) {
|
||||||
|
updates=[{message}];assert.equal(await kodIleBagla(code,window),null);
|
||||||
|
}
|
||||||
|
updates=[{edited_message:good}];assert.equal(await kodIleBagla(code,window),null);
|
||||||
|
updates=[{message:good}];assert.equal((await kodIleBagla(code,window)).chatId,'123');
|
||||||
|
updates=[{message:{...good,text:'/start '+code}}];assert.equal((await kodIleBagla(code,window)).ad,'QA');
|
||||||
|
} finally {globalThis.fetch=oldFetch;if(oldToken===undefined)delete process.env.TELEGRAM_BOT_TOKEN;else process.env.TELEGRAM_BOT_TOKEN=oldToken;}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user