feat(api): gömülebilir katalog widget'ı (Faz 2 POC)
- GET /widget/loader.js: müşterinin yapıştırdığı tek script — iframe kurar,
postMessage köprüsü (yükseklik + sase:partClick/sase:decoded olayları),
parça tıklamasını data-part-click-url şablonuna ({code},{brand}) yönlendirir,
window.saseWidget.onPartClick/onDecode callback'leri; CORP cross-origin
(helmet same-origin varsayılanı müşteri sitesinde script'i bloklardı)
- GET /widget?key=sase_pk_...: iframe shell — vanilla JS katalog uygulaması
(VIN formu, çok-aday seçimi, kategori grid + breadcrumb, parça tablosu,
şema görseli, açık/koyu tema). Domain kilidi = anahtarın metadata.domains
listesinden üretilen frame-ancestors CSP'si (tarayıcı zorlar); publishable
olmayan anahtar 403; X-Frame-Options bu cevapta kaldırılır
- internal-admin: publishable:true + domains[] ile sase_pk_ anahtar üretimi
- api.sase.tr kısa yolu: /widget* → /api/widget*
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,11 @@ export class ApiKeysAdminController {
|
||||
* Anahtar üret. Kullanıcı `userId` veya `email` ile seçilir (anahtar o
|
||||
* kullanıcının aboneliği/marka yetkisiyle çalışır). `dailyDecodeLimit`
|
||||
* anahtar metadata'sına yazılır; verilmezse env/100 varsayılanı geçerli.
|
||||
*
|
||||
* `publishable: true` → widget anahtarı (sase_pk_): müşteri sayfasında
|
||||
* görünür, /widget shell'i yalnız bunları kabul eder ve `domains`
|
||||
* listesinden frame-ancestors CSP kilidi üretir. domains zorunludur
|
||||
* (boş liste = hiçbir 3. parti sitede render olmaz, sadece test).
|
||||
*/
|
||||
@Post()
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@@ -50,6 +55,8 @@ export class ApiKeysAdminController {
|
||||
expiresInDays?: number;
|
||||
rateLimitMax?: number;
|
||||
test?: boolean;
|
||||
publishable?: boolean;
|
||||
domains?: string[];
|
||||
},
|
||||
) {
|
||||
const user = await this.resolveUser(body);
|
||||
@@ -60,19 +67,30 @@ export class ApiKeysAdminController {
|
||||
) {
|
||||
throw new BadRequestException("dailyDecodeLimit pozitif bir sayı olmalı");
|
||||
}
|
||||
if (body.publishable && body.domains !== undefined && !Array.isArray(body.domains)) {
|
||||
throw new BadRequestException('domains bir dizi olmalı (ör. ["musteri.com"])');
|
||||
}
|
||||
if (body.domains && !body.publishable) {
|
||||
throw new BadRequestException("domains yalnız publishable anahtarlarda kullanılır");
|
||||
}
|
||||
|
||||
const metadata: Record<string, unknown> = {};
|
||||
if (body.dailyDecodeLimit) metadata.dailyDecodeLimit = Math.floor(body.dailyDecodeLimit);
|
||||
if (body.publishable) {
|
||||
metadata.publishable = true;
|
||||
metadata.domains = (body.domains ?? []).filter((d) => typeof d === "string" && d.trim());
|
||||
}
|
||||
|
||||
const created = await getApiKeyApi().createApiKey({
|
||||
body: {
|
||||
userId: user.id,
|
||||
name: body.name ?? `${user.email} public-api`,
|
||||
prefix: body.test ? "sase_test_" : "sase_live_",
|
||||
prefix: body.publishable ? "sase_pk_" : body.test ? "sase_test_" : "sase_live_",
|
||||
...(body.expiresInDays ? { expiresIn: body.expiresInDays * DAY_SECONDS } : {}),
|
||||
...(body.rateLimitMax
|
||||
? { rateLimitEnabled: true, rateLimitTimeWindow: 60_000, rateLimitMax: body.rateLimitMax }
|
||||
: {}),
|
||||
...(body.dailyDecodeLimit
|
||||
? { metadata: { dailyDecodeLimit: Math.floor(body.dailyDecodeLimit) } }
|
||||
: {}),
|
||||
...(Object.keys(metadata).length ? { metadata } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -85,6 +103,7 @@ export class ApiKeysAdminController {
|
||||
name: created.name,
|
||||
expiresAt: created.expiresAt,
|
||||
dailyDecodeLimit: body.dailyDecodeLimit ?? null,
|
||||
...(body.publishable ? { publishable: true, domains: metadata.domains } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user