1 · OAuth Connect (/connect)

Tenant eklentiyi kurduğunda, Restomenum tarayıcıyı senin connectUrl'ine tek-kullanımlık bir code ile yönlendirir. Bu code'u sunucu tarafında credential'a çevirirsin (bir sonraki adım). Bu, eklentinin etkinleşmesi için zorunlu kapıdır.

Akış

Tenant: "Eklentiyi kur / Bağlan"
   │
   ▼
Restomenum  ──redirect──►  https://<senin-origin>/connect
                              ?code=<tek-kullanımlık>
                              &environment=sandbox|production   ◄── API kökünü BU belirler
                              &state=<csrf>
   │
   ▼
Senin /connect (sunucu):
   1) state SENİN ürettiğin biri mi? → evet: CSRF doğrula · hayır (platform-issued): atla
   2) code'u Token exchange ile credential'a çevir  (adım 2)
   3) credential'ı tenant başına sakla
   4) kendi paneline yönlendir

Query parametreleri

Restomenum, connectUrl'ine tarayıcıyı GET ile şu query parametreleriyle yönlendirir:

AlanTipZorunluAçıklama
codestring✓Tek-kullanımlık, kısa ömürlü yetkilendirme kodu. Token exchange'de credential'a çevrilir; ikinci kullanımda invalid_grant.
environment"sandbox" | "production"✓Kurulumun ortamı. Token takasını bu ortamın köküne gönder ve kurulumu bu ortamla sakla. Bkz. Ortamlar.
statestring–CSRF state. Değeri opak kabul et — formatı platform dayatmaz ve üreteni kurulumu kimin başlattığına bağlıdır (aşağıdaki uyarı).
environment'ı yok sayma. Tek bir ortama sabitlenmiş kod, production kurulumunda token takasını yanlış API köküne gönderir ve kurulum çalışmaz. Kökü daima gelen değerden seç: sandbox → https://sandbox.plugins.restomenum.app, production → https://plugins.restomenum.app.

Başka bir parametreye güvenme: tenant kimliği (tenantId) ve verilen yetkiler (scopes) URL'den değil, token exchange yanıtından gelir — sorgu dizisi tarayıcıdan geçtiği için otoriter değildir.

state'i kim üretir?

state'i her zaman sen üretmezsin. Kurulumu marketplace başlattığında state'i platform üretir ve sana opak gelir — senin CSRF deponda karşılığı yoktur. Bu durumda koşulsuz bir verifyState(state) kontrolü her kurulumu 400 ile öldürür.
Kurulumu başlatanstate'i üretenSen ne yaparsın
Marketplace (tenant "Kur" der)PlatformKendi CSRF kontrolünü atla — değeri doğrulayamazsın
Sen (kendi sitendeki "Restomenum'a ekle")SenÜret + oturuma yaz → dönüşte timing-safe karşılaştır + tek-kullan

İkisini ayırmanın pratik yolu: kendi state'ini seçtiğin bir önekle üret (örn. myapp1.) ve yalnız o öneki taşıyanı doğrula. Önek senin seçimindir; platform bir format dayatmaz.

CSRF kontrolünü atlamak akışı savunmasız bırakmaz: code tek-kullanımlık ve kısa ömürlüdür, credential'a çevrilmesi sunucu-sunucu bir çağrı gerektirir ve client_secret tarayıcıya hiç gitmez.

Referans — /connect (Node)

// /connect — OAuth Connect redirect hedefi (Node + Express)
// Tenant eklentiyi kurunca tarayıcı buraya ?code=..&environment=..&state=.. ile gelir.
// client_secret SADECE burada (sunucuda) kullanılır; tarayıcıya ASLA gitmez.
import express from 'express';
const app = express();

const CLIENT_ID = process.env.RESTOMENUM_CLIENT_ID;       // = pluginId (UUID) — slug DEĞİL
const CLIENT_SECRET = process.env.RESTOMENUM_CLIENT_SECRET; // portalda üretilen cs_...

// API kökü ORTAMA göre seçilir — tek bir köke SABİTLEME (production kurulumu yanlış köke gider).
const BASES = {
  sandbox: 'https://sandbox.plugins.restomenum.app',
  production: 'https://plugins.restomenum.app',
};

// Kendi ürettiğin state'leri tanımak için SENİN seçtiğin önek (platform bir format dayatmaz).
const MY_STATE_PREFIX = 'myapp1.';

app.get('/connect', async (req, res) => {
  const { code, environment, state } = req.query;
  if (!code) return res.status(400).send('missing code');

  // 0) ORTAM: kurulumun hangi ortama ait olduğunu SORGUDAN öğrenirsin ("sandbox" | "production").
  //    Kökü buradan seç ve kurulumu bu ortamla sakla — AYNI tenantId iki ortamda birden kurulu
  //    olabilir ve her kurulumun apiKey/webhookSecret'ı AYRIDIR.
  const env = environment === 'production' ? 'production' : 'sandbox';
  const TOKEN_URL = `${BASES[env]}/plugin-api/oauth/token`;

  // 1) CSRF state — ⚠️ state'i HER ZAMAN SEN üretmezsin:
  //    • Kurulumu MARKETPLACE başlatırsa state'i PLATFORM üretir ve sana OPAK gelir; senin CSRF
  //      deponda karşılığı YOKTUR → KOŞULSUZ doğrularsan her kurulum 400 ile ölür.
  //    • Kurulumu SEN başlatırsan (kendi sitendeki "Restomenum'a ekle" akışı) state SENİNDİR.
  //    Bu yüzden yalnız KENDİ önekini taşıyan state'i doğrula:
  if (typeof state === 'string' && state.startsWith(MY_STATE_PREFIX)) {
    if (!consumeState(state)) return res.status(400).send('invalid state'); // timing-safe + tek-kullan
  }
  // Önek yoksa (platform-issued) kendi CSRF kontrolünü ATLA — akışın güvenliği tek-kullanımlık,
  // kısa ömürlü code'a ve sunucu-sunucu exchange'e dayanır (client_secret tarayıcıya hiç gitmez).

  // 2) code → credential exchange (server-to-server)
  const r = await fetch(TOKEN_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      grant_type: 'authorization_code',
      code,
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
    }),
  });
  if (!r.ok) return res.status(502).send('exchange failed');

  // 2b) ZARF: BAŞARI yanıtı DAİMA zarflıdır — alanlar "data" İÇİNDEDİR, kökte değil.
  //     Düz gövde VARSAYMA: kökten okuyan kod alanları undefined görür ve canlıda patlar.
  //     ("?? body" geriye uyum için zararsız; başarı yolunda ikinci dala düşmez.)
  const body = await r.json();
  const { apiKey, webhookSecret, tenantId, scopes, environment } = body.data ?? body;

  // 3) tenant + ORTAM başına SAKLA (apiKey: Callback API çağrıları, webhookSecret: imza doğrulama).
  //    Anahtar yalnız tenantId OLMAMALI — ikinci ortamın kurulumu birincinin üzerine yazar.
  //    Yanıttaki "environment" KOŞULSUZ gelir ve /connect'e gelen "env" ile AYNI kaynaktan türer
  //    → sapmamalı. Saparsa bu bir PLATFORM hatasıdır: sandbox credential'ıyla production trafigi
  //    calisiyormus gibi gorunur ve ancak gercek bir tahsilat bozulunca fark edilir → kaydetme, patla.
  if (environment !== env) throw new Error('ortam uyusmazligi (platform hatasi)');
  await saveInstallCredentials({ tenantId, environment, apiKey, webhookSecret, scopes });

  res.redirect('/panel?connected=1'); // kendi paneline dön
});

Adımlar

  1. Manifest'te Connect path tanımla (sürümün Origin'i + path).
  2. state'i koşullu doğrula: yalnız senin ürettiğin (kendi önekini taşıyan) state CSRF kontrolünden geçer; marketplace kurulumunda gelen platform state'i opaktır ve doğrulanmaz.
  3. Gelen code'u Token exchange ile değiştir.
  4. Connect tamamlanmadan eklenti aktif olmaz; akışı eksiksiz tamamla.
client_secret yalnız sunucuda kullanılır; tarayıcıya/iframe'e asla gönderme. codetek kullanımlıktır ve kısa ömürlüdür.
Bu akışı yayınlamadan denemek için Test Mağazaları (Dev Stores) kullan — yayınlanmamış eklentini kendi test restoranında kurup connect'i uçtan uca test edebilirsin.