E

Entegrasyon Dökümantasyonu

Eyena Embed API — tam rehber

A — İçerik Yönetimi (Soru & Ödev CRUD)

Bu bölüm kimler için?

Kendi sorularınızı ve ödevlerinizi Eyena'ya programatik olarak eklemek isteyen 3. taraf entegratörler. Eyena Sistem Yetkilisi bu işlemleri portal üzerinden de yapabilir; API yolu otomasyon/toplu içerik yükleme için kullanışlıdır.

0

Ön Koşul — Kimlik bilgilerini alın

Eyena Sistem Yetkilisi'nden client_id (embed-… formatı) ve client_secret alın. Bu kimlikler hem içerik yönetimi hem de iframe oturumları için kullanılır.

1

M2M access token alın (tüm API çağrıları için)

Client credentials akışıyla eyena-giris'ten bir access token alın. Bu istek kesinlikle sunucu tarafında yapılmalıdır.

// Sunucu tarafı
const res = await fetch('https://giris.eyena.net/connect/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'client_credentials',
    client_id:     'embed-abc123',    // Sistem Yetkilisi'nden
    client_secret: 'GIZLI_SECRET',   // .env'de tutun
    scope: 'api',
  }),
})
const { access_token } = await res.json()
2

Soru oluşturun

Soruların içeriği tek bir görseldir; istek multipart/form-dataolarak gönderilmelidir. Sorunun sahibi otomatik olarak token'daki client_id olur — başka bir istemcinin sorusunu oluşturmanız veya görmeniz mümkün değildir.

// POST https://api.eyena.net/api/embed/sorular   (multipart/form-data)
const form = new FormData()
form.append('Baslik', '1. Soru')
form.append('Gorsel', gorselDosyasi)          // File — JPEG/PNG/GIF/WebP, max 5 MB
form.append('SeceneklerJson', JSON.stringify([
  { metin: 'Birinci şık', dogruMu: true,  sira: 1 },
  { metin: 'İkinci şık',  dogruMu: false, sira: 2 },
  { metin: 'Üçüncü şık', dogruMu: false, sira: 3 },
]))
form.append('BolgelerJson', JSON.stringify([  // opsiyonel — heatmap analizi için ilgi alanları
  { x: 10, y: 20, genislik: 200, yukseklik: 150, aciklama: 'Grafik', sira: 1 },
]))

const res = await fetch('https://api.eyena.net/api/embed/sorular', {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${access_token}` },
  // Content-Type başlığını EKLEMEYİN — tarayıcı/fetch boundary'yi otomatik ayarlar
  body: form,
})
const soru = await res.json()
// soru.id  ← ödevinize eklemek için bu ID'yi saklayın

Görsel Azure Blob Storage'a yüklenir; GET /api/embed/sorular/{id} yanıtındaki icerikalanı zaten çözümlenmiş bir URL içerir. JPEG, PNG, GIF ve WebP desteklenir; maksimum boyut 5 MB'dır.

3

Soru listeleyin / güncelleyin / silin

// Kendi sorularınızı listele
GET https://api.eyena.net/api/embed/sorular

// Tek soru detayı (icerik = çözümlenmiş görsel URL'si)
GET https://api.eyena.net/api/embed/sorular/{id}

// Güncelle  (multipart/form-data)
// Gorsel alanı opsiyoneldir — gönderilmezse mevcut görsel korunur;
// gönderilirse eski görsel silinir, yenisi yüklenir.
const form = new FormData()
form.append('Baslik', 'Güncel başlık')
form.append('Gorsel', yeniGorselDosyasi)   // opsiyonel
form.append('SeceneklerJson', JSON.stringify([...]))
form.append('BolgelerJson',   JSON.stringify([...]))

await fetch(`https://api.eyena.net/api/embed/sorular/${soru.id}`, {
  method: 'PUT',
  headers: { 'Authorization': `Bearer ${access_token}` },
  body: form,
})

// Sil — görsel de otomatik olarak silinir
// (ödev bağlantısı varsa HTTP 409 döner — önce ödevden çıkarın)
DELETE https://api.eyena.net/api/embed/sorular/{id}

Tüm istekler Authorization: Bearer {access_token} başlığı gerektirir ve yalnızca kendi istemcinize ait sorularla çalışır.

4

Ödev oluşturun ve sorularla doldurun

Ödevlerin slug'ı istemci içinde benzersiz olmalıdır.soruIdsiçinde yalnızca kendi sorularınızın ID'leri yer alabilir — başka istemcinin sorusu HTTP 400 döner.

// POST https://api.eyena.net/api/embed/odevler
const res = await fetch('https://api.eyena.net/api/embed/odevler', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${access_token}`,
  },
  body: JSON.stringify({
    slug:    'turkce-9-1',   // URL'de kullanılır — küçük harf, rakam, tire
    baslik:  'Türkçe 9. Sınıf Test 1',
    aktif:   true,
    soruIds: [soru.id, ...],   // kendi sorularınızın ID'leri, sıra önemli
  }),
})
const odev = await res.json()
// odev.slug  ← iframe entegrasyonunda kullanılacak slug
5

Ödev listeleyin / güncelleyin / silin

// Kendi ödevlerinizi listele
GET https://api.eyena.net/api/embed/odevler

// Tek ödev detayı (sorularıyla birlikte)
GET https://api.eyena.net/api/embed/odevler/{id}

// Güncelle (soru listesini tamamen yeniler)
PUT https://api.eyena.net/api/embed/odevler/{id}
// Body: { slug, baslik, aktif, soruIds }

// Sil
DELETE https://api.eyena.net/api/embed/odevler/{id}

B — İframe Entegrasyonu (Test Akışı)

Genel Akış

  1. Sunucunuz, eyena-giris'ten M2M access token alır.
  2. Sunucunuz, bu token ile eyena-api'den kısa ömürlü embed token alır.
  3. Tarayıcı (frontend), embed token ile iframe'i yükler.
  4. Tarayıcı, test bitince postMessage ile sonuçları dinler.
  5. Sunucunuz (opsiyonel), aynı token ile soru görselini/içeriğini çeker.

client_secret asla tarayıcıya gönderilmez — tüm gizli adımlar sunucu tarafında kalır.

0

Ön Koşul — Ödev hazır olmalı

İframe'e yüklemek istediğiniz ödev ya A bölümündeki CRUD API ile ya da Eyena Sistem Yetkilisi'nin portaldaki "Embed Ödevler" ekranıyla oluşturulmuş ve aktif: trueolmalıdır. Slug'ı not edin — bir sonraki adımda kullanılır.

1

M2M access token alın (Sunucu tarafı)

A bölümündeki Adım 1 ile aynı — tek bir token hem CRUD hem de embed oturumları için geçerlidir.

const res = await fetch('https://giris.eyena.net/connect/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'client_credentials',
    client_id: 'embed-abc123',
    client_secret: 'GIZLI_SECRET',
    scope: 'api',
  }),
})
const { access_token } = await res.json()
2

Embed token alın (Sunucu tarafı)

Access token ile, belirli bir ödev ve öğrenci için kısa ömürlü (2 saat) bir embed token isteyin. Origin başlığı, kayıtlı izinli origin ile birebiraynı olmalıdır. Ödev otomatik olarak token'daki istemciye göre çözümlenir — aynı slug başka bir istemciye ait ise HTTP 404 döner.

const res = await fetch('https://api.eyena.net/api/embed/session', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${access_token}`,
    'Origin': 'https://siteniz.com',    // kayıtlı izinli origin ile aynı
  },
  body: JSON.stringify({
    slug: 'turkce-9-1',         // embed ödev slug'ı (zorunlu)
    ogrenciAdi: 'Ali Yılmaz',   // öğrenci adı — zorunlu, sonuçlarda geri döner
  }),
})
const { embedToken, expiresAt, baslik } = await res.json()
// embedToken'ı frontend'e döndürün (client_secret'ı ASLA döndürmeyin)

Origin kayıtlı değilse istek HTTP 403 ile reddedilir. Slug başka bir istemciye aitse HTTP 404 döner.

3

iframe'i oluşturun (Frontend)

Embed token'ı kendi backend'inizden alıp iframe'in src'sine yerleştirin. allow="camera; fullscreen" özniteliği, göz izleme kalibrasyonu için zorunludur.

<iframe
  id="eyena-frame"
  style="width: 100%; height: 600px; border: none;"
  allow="camera; fullscreen"
></iframe>

<script>
  async function eyenaTestiYukle(slug) {
    // 1. Embed token'ı KENDİ backend'inizden isteyin
    const res = await fetch('/api/eyena-token?slug=' + slug)
    const { embedToken } = await res.json()

    // 2. iframe src'sini ayarlayın
    document.getElementById('eyena-frame').src =
      'https://portal.eyena.net/embed/' + slug + '?token=' + embedToken
  }

  eyenaTestiYukle('turkce-9-1')
</script>
4

Testi komutla başlatın (Frontend)

iframe yüklenince size eyena-embed-ready olayı gönderir. Buna karşılık eyena-embed-startkomutunu gönderdiğinizde test, iframe içinde herhangi bir tıklama olmadan başlar — böylece iframe'i gizleseniz bile akışı parent sayfadan yönetebilirsiniz.

const frame = document.getElementById('eyena-frame')

window.addEventListener('message', (event) => {
  if (event.origin !== 'https://portal.eyena.net') return

  if (event.data?.type === 'eyena-embed-ready') {
    frame.contentWindow.postMessage(
      { type: 'eyena-embed-start' },
      'https://portal.eyena.net',
    )
  }
})

Göz izleme (Tobii) tam ekran + kamera izni ister; tarayıcılar bunları yalnızca gerçek bir kullanıcı tıklamasından sonra verir. Tamamen gizli/otomatik akış güvenilir biçimde yalnızca göz izleme olmayan testlerde çalışır.

5

Sonuçları dinleyin (Frontend)

Öğrenci testi tamamlayınca iframe, postMessageile parent sayfaya iki olay gönderir. Payload'a güvenmeden önce her zaman event.origin'u doğrulayın.

window.addEventListener('message', (event) => {
  if (event.origin !== 'https://portal.eyena.net') return

  if (event.data?.type === 'eyena-embed-result') {
    const { schemaVersion, ogrenciAdi, answers, cards, regions, gaze, heatmaps } = event.data
    // answers:  [{ soruId, secenekId }]  (secenekId boşsa null)
    // cards:    [{ soruId, image, width, height, scale, coordSpace: 'card' }]
    //   image: OPAK JPEG data URL — öğrencinin gördüğü kartın TAMAMI
    //   (soru + görsel + şıklar). Diğer tüm katmanlar bu kutuya göre ölçülür.
    // regions:  [{ soruId, kind: 'soru'|'secenek', secenekId, x, y, w, h }]
    // gaze:     [{ soruId, coordSpace, points: [{ x, y, timestamp, secenekId }] }]
    // heatmaps: [{ soruId, image, width, height }]  (şeffaf PNG, veri yoksa null)

    // Arka plan olarak cards[].image kullanın — soru görselini (icerik) DEĞİL.
    // icerik şıkları içermez ve kartla aynı kutu değildir; üzerine bindirilen
    // heatmap hizalanmaz, şıklara bakılan süre görselin dışında kalır.
    const card = cards.find((c) => c.soruId === soruId)
    canvas.width = card.width      // CSS px — koordinat kutusu
    canvas.height = card.height
    ctx.drawImage(cardImg, 0, 0, card.width, card.height)
    ctx.drawImage(heatmapImg, 0, 0, card.width, card.height)

    console.log('Sonuç:', event.data)
  }

  if (event.data?.type === 'eyena-embed-complete') {
    console.log('Test tamamlandı:', event.data.slug)
  }
})

Göz izleme verisi (gaze) yalnızca Tobii aktifken ve iframe'e kamera/tam ekran izni verildiğinde dolar. Aksi halde test yine çalışır ama gaze dizisi boş, heatmaps[].image alanları null gelir.cards[].imagegöz izlemeden bağımsızdır ve her durumda gelir. Kart görselleri, öğrenci sorudan ayrılırken (son soru için Tamamla'da) alınır — seçtiği şık dahil, bıraktığı hâliyle. Çizimleri görsele işlenmez; ayrı olarak drawings alanında gelir. Tüm görseller tamamen tarayıcıda üretilir ve postMessage ile aktarılır — sunucuya hiçbir görsel kaydedilmez.

6

Soru içeriğini token ile alın (Sunucu tarafı — opsiyonel)

postMessagepayload'ı yalnızca ham veri taşır — soru görseli iframe'den geçmez. İçeriği kendi tarafınızda zaten tutuyorsanız bu adım genellikle gereksizdir; demo/önizleme senaryoları için aynı embed token'ı kullanabilirsiniz.

// Sunucu tarafı — embed token'ı query string'de geçirin
const res = await fetch(
  'https://api.eyena.net/api/embed/coz?token=' + encodeURIComponent(embedToken),
  { cache: 'no-store' },
)
const detay = await res.json()

Yanıt gövdesi:

{
  "slug":      "turkce-9-1",
  "baslik":    "Türkçe 9. Sınıf - 1. Ünite",
  "ogrenciAdi": "Ali Yılmaz",
  "sorular": [
    {
      "id":       "a1b2…",          // postMessage'taki soruId ile eşleşir
      "sira":     1,
      "baslik":   "1. Soru",
      "icerik":   "https://…",      // blob referansı çözülmüş URL
      "secenekler": [
        { "id": "c1…", "metin": "Birinci şık",  "sira": 1 },
        { "id": "c2…", "metin": "İkinci şık",   "sira": 2 }
      ],
      "bolgeler": [
        { "x": 10, "y": 20, "genislik": 200, "yukseklik": 150,
          "aciklama": "Grafik", "sira": 1 }
      ]
    }
  ]
}

Token kendisi yetkilendirmedir — ayrıca access token gerekmez. Geçersiz/süresi dolmuş token HTTP 401, istemciye ait olmayan slug HTTP 404 döner.


Güvenlik Notları

  • client_secret ve access_token asla frontend kodunda yer almamalı.
  • Her sayfa yüklemesinde taze bir embed tokenüretin; token'ları önbelleğe almayın.
  • Embed token origin'e bağlıdır; çalınsa bile başka bir alan adından yüklenemez.
  • Token'lar 2 saat sonra geçersiz olur.
  • CRUD endpointleri istemci kapsamlıdır — bir istemci başka istemcinin soru veya ödevine erişemez, oluşturamaz, değiştiremez.

Endpoint Özeti

YöntemEndpointAçıklamaAuth
POST/connect/tokenM2M access token alclient_credentials
POST/api/embed/sessionEmbed (iframe) token alBearer access_token
GET/api/embed/coz?token=…Ödev detayını alembed token (query)
GET/api/embed/sorularSoruları listeleBearer access_token
POST/api/embed/sorularSoru oluştur (multipart/form-data)Bearer access_token
GET/api/embed/sorular/{id}Soru detayıBearer access_token
PUT/api/embed/sorular/{id}Soru güncelle (multipart/form-data)Bearer access_token
DELETE/api/embed/sorular/{id}Soru sil (görsel de silinir)Bearer access_token
GET/api/embed/odevlerÖdevleri listeleBearer access_token
POST/api/embed/odevlerÖdev oluşturBearer access_token
GET/api/embed/odevler/{id}Ödev detayıBearer access_token
PUT/api/embed/odevler/{id}Ödev güncelleBearer access_token
DELETE/api/embed/odevler/{id}Ödev silBearer access_token

/connect/token ve /api/embed/session istekleri https://giris.eyena.net / https://api.eyena.net adresleri üzerinden gönderilir. Diğer tüm endpointler https://api.eyena.net altındadır.