feat: sunucu tarafı hesap altyapısı — Stripe webhook, kotalar, gerçek API anahtarı ve belge deposu
Denetlenen 5 teknik borç kalemi de kapatıldı:
1) Stripe webhook + gerçek yetkilendirme (ÖNCEDEN YOKTU)
- POST /api/billing/webhook — SDK'sız HMAC-SHA256 imza doğrulaması,
tekrar saldırısına karşı ±300sn zaman penceresi, timingSafeEqual
- checkout.session.completed / subscription.created|updated|deleted işlenir
- Ödeme → kullanıcının planı sunucuda güncellenir (önce ödeme hiçbir şeyi
değiştirmiyordu)
2) Plan kotaları GERÇEKTEN uygulanıyor (önce sadece tanımlıydı)
- server/entitlements.mjs: plan hakları + kotaKontrol()
- ekip, belge adedi/boyutu, API anahtarı kotaları sunucuda zorlanır
- Kota aşımında 402 + anlaşılır Türkçe mesaj; arayüzde KotaUyarisi bandı
- /api/me/plan: plan, haklar ve gerçek kullanım
3) Kullanıcı verisi localStorage'dan sunucuya taşındı
- server/user-store.mjs: kullanici_verisi tablosu (izinli anahtar listesi)
- AppContext girişte sunucudan yükler, değişiklikleri sunucuya yazar;
localStorage yalnızca çevrimdışı önbellek
- Artık cihaz değişince veri kaybolmuyor
4) API anahtarları gerçek (önce Math.random ile tarayıcıda üretiliyordu)
- Sunucuda crypto.randomBytes ile üretilir, YALNIZCA sha256 hash'i saklanır
- Ham değer bir kez gösterilir; verifyApiKey ile doğrulanır, son kullanım
izlenir, iptal anında geçerli olur
5) Belge deposu gerçek (önce boyut Math.random'dı, dosya hiç yüklenmiyordu)
- Dosyalar kalıcı volume'e yazılır, boyut GERÇEK bayt
- Auth'lu indirme (blob), silme diskten de siler, kota sunucuda uygulanır
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Sunucu tarafı hesap servisleri: plan/kota, kullanıcı verisi, API anahtarları, belgeler.
|
||||
* (Önceden bunların hepsi localStorage'daydı veya sahteydi.)
|
||||
*/
|
||||
import { getToken } from './auth';
|
||||
|
||||
const auth = () => ({ Authorization: `Bearer ${getToken()}`, 'Content-Type': 'application/json' });
|
||||
|
||||
async function iste(url, opts = {}) {
|
||||
const res = await fetch(url, { ...opts, headers: { ...auth(), ...(opts.headers || {}) } });
|
||||
const d = await res.json().catch(() => ({}));
|
||||
if (!d.success) {
|
||||
const e = new Error(d.error || `İstek başarısız (HTTP ${res.status})`);
|
||||
e.kod = d.kod;
|
||||
throw e;
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
// --- Plan & kota ---
|
||||
export const getPlan = () => iste('/api/me/plan');
|
||||
|
||||
// --- Kullanıcı verisi ---
|
||||
export const getAllUserData = () => iste('/api/me/data').then((d) => d.veri || {});
|
||||
export const setUserData = (anahtar, deger) =>
|
||||
iste('/api/me/data', { method: 'PUT', body: JSON.stringify({ anahtar, deger }) });
|
||||
|
||||
// --- API anahtarları ---
|
||||
export const listApiKeys = () => iste('/api/me/apikeys');
|
||||
export const createApiKey = (ad) => iste('/api/me/apikeys', { method: 'POST', body: JSON.stringify({ ad }) });
|
||||
export const revokeApiKey = (id) => iste(`/api/me/apikeys?id=${encodeURIComponent(id)}`, { method: 'DELETE' });
|
||||
|
||||
// --- Belgeler ---
|
||||
export const listDocuments = () => iste('/api/me/documents');
|
||||
export const deleteDocument = (id) => iste(`/api/me/documents?id=${encodeURIComponent(id)}`, { method: 'DELETE' });
|
||||
|
||||
export async function uploadDocument(file, kategori) {
|
||||
const icerikBase64 = await new Promise((resolve, reject) => {
|
||||
const r = new FileReader();
|
||||
r.onload = () => resolve(String(r.result).split(',')[1]);
|
||||
r.onerror = reject;
|
||||
r.readAsDataURL(file);
|
||||
});
|
||||
return iste('/api/me/documents', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ad: file.name, mime: file.type, kategori, icerikBase64 }),
|
||||
});
|
||||
}
|
||||
|
||||
/** Auth başlığı gerektiği için <a href> yerine blob ile indirilir. */
|
||||
export async function downloadDocument(id, ad) {
|
||||
const res = await fetch(`/api/me/documents/download?id=${encodeURIComponent(id)}`, {
|
||||
headers: { Authorization: `Bearer ${getToken()}` },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const d = await res.json().catch(() => ({}));
|
||||
throw new Error(d.error || `İndirilemedi (HTTP ${res.status})`);
|
||||
}
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = ad || 'belge';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 5000);
|
||||
}
|
||||
|
||||
export function formatBayt(b) {
|
||||
if (b == null) return '—';
|
||||
if (b < 1024) return `${b} B`;
|
||||
if (b < 1024 * 1024) return `${(b / 1024).toFixed(1)} KB`;
|
||||
if (b < 1024 * 1024 * 1024) return `${(b / 1024 / 1024).toFixed(1)} MB`;
|
||||
return `${(b / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
||||
}
|
||||
Reference in New Issue
Block a user