feat(api): public API POC — API-key auth + /api/v1 decode & OEM xref

Kapalı-beta public API temeli (rapor: api-widget-analizi-2026-07-03):
- better-auth apiKey plugin (defaultPrefix sase_, enableMetadata, anahtar
  başına 120 istek/dk); enableSessionForAPIKeys KAPALI — anahtar dashboard
  oturumu yerine geçmez, cookie AuthGuard davranışı değişmez
- apikeys tablosu (migration 0023, plugin şemasının birebir karşılığı)
- ApiKeyGuard: Authorization Bearer / x-api-key → verifyApiKey → kullanıcı
  yüklenir (status kontrolü), request.user + request.apiKey doldurulur
- /api/v1/vin/decode: mevcut slim kontrat + aday akışı; günlük başarılı-decode
  kotası (Redis, TR günü, aynı VIN idempotent; limit: key metadata
  dailyDecodeLimit → PUBLIC_API_DAILY_DECODE_LIMIT → 100) ve
  X-Decode-Quota-* header'ları
- /api/v1/oem/:code: P çapraz-referans (kotasız)
- internal/admin/api-keys: x-internal-token ile anahtar üret/listele/kapat
  (düz metin yalnız create cevabında)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 16:15:17 +03:00
parent 371aed3645
commit 7a4fe51d09
12 changed files with 637 additions and 2 deletions

View File

@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
import { generateReferralCode, normalizeName } from "@sase/shared";
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { captcha } from "better-auth/plugins";
import { apiKey, captcha } from "better-auth/plugins";
import { eq } from "drizzle-orm";
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
@@ -224,6 +224,21 @@ export function createAuth(
"http://localhost:4000",
],
plugins: [
// Public API anahtarları (api.sase.tr / /api/v1). Doğrulama ApiKeyGuard
// üzerinden auth.api.verifyApiKey ile yapılır; enableSessionForAPIKeys
// bilinçli olarak KAPALI — API anahtarı dashboard oturumu yerine geçmez,
// cookie tabanlı AuthGuard'ın davranışı değişmez.
apiKey({
defaultPrefix: "sase_",
enableMetadata: true,
// Anahtar başına istek tavanı (verify başına sayaç). Günlük decode
// kotasından ayrıdır — o, PublicApiQuotaService'te Redis ile tutulur.
rateLimit: {
enabled: true,
timeWindow: 60_000, // 1 dakika
maxRequests: 120,
},
}),
// Cloudflare Turnstile: yalnızca secret tanımlıysa aktif. sign-in/sign-up
// uçları "x-captcha-response" header'ındaki token ile doğrulanır.
...(process.env.TURNSTILE_SECRET_KEY
@@ -247,3 +262,42 @@ export function getAuth() {
}
return authInstance;
}
/** apiKey plugin'inin döndürdüğü anahtar kaydı (verify/create cevapları). */
export interface ApiKeyRecord {
id: string;
userId: string;
name: string | null;
start: string | null;
prefix: string | null;
enabled: boolean;
expiresAt: Date | null;
metadata: Record<string, unknown> | null;
}
/**
* apiKey plugin uçları. `authInstance` geniş `ReturnType<typeof betterAuth>`
* ile tiplendiği için plugin uçları tipte görünmüyor (runtime'da mevcutlar);
* bu erişimci onları dar ve doğru imzalarla expose eder.
*/
export function getApiKeyApi() {
return getAuth().api as unknown as {
verifyApiKey: (input: { body: { key: string } }) => Promise<{
valid: boolean;
error: { message?: string; code: string } | null;
key: ApiKeyRecord | null;
}>;
createApiKey: (input: {
body: {
userId: string;
name?: string;
prefix?: string;
expiresIn?: number;
metadata?: Record<string, unknown>;
rateLimitEnabled?: boolean;
rateLimitTimeWindow?: number;
rateLimitMax?: number;
};
}) => Promise<ApiKeyRecord & { key: string }>;
};
}