İleri
guvenlik/guvenli-dosya-yukleme
Güvenli Dosya Yükleme Sistemi
- PHP 8
- PDO
- MySQL
- finfo
- +3
195 inceleme 75 canlı açılış 1 beğeni
Güvenlik Orta
Her değişikliğin izini bırakan, kendisi değiştirilemeyen kayıt. Tek yazma noktası, alan bazlı diff, hassas alan maskeleme ve veriyle aynı transaction içinde yazılan append-only denetim tablosu.
Veritabanı şeması projedeki cy_audit.sql dosyasında.
ERP, muhasebe ve yönetim panellerinde en çok istenen özellik "işlem geçmişi"dir. İnternette bulacağınız örneklerin çoğu ise şunu yapar: bir logs tablosu açar, içine "Ahmet ürünü güncelledi" gibi bir cümle yazar ve biter.
O cümle üç soruyu da cevapsız bırakır: hangi alan değişti, neydi, ne oldu? Üstelik o tabloya DELETE atmanın önünde hiçbir engel yoktur — yani "bu fiyatı kim değiştirdi?" sorusunun cevabı, o cevabı vermek istemeyen kişi tarafından silinebilir. Böyle bir kayıt denetim değil, süstür.
Bu proje o üç soruyu da cevaplayan bir denetim katmanının nasıl kurulacağını gösteriyor: audit_log tablosu append-only'dir (uygulamada o tabloya UPDATE/DELETE yapan hiçbir yol yoktur), kayıt alan bazlı diff tutar, hassas alanlar loga yazılmadan önce maskelenir ve veri ile kaydı aynı transaction içinde yazılır — biri başarısız olursa ikisi de geri alınır.
Kimler için uygun?
Klonla, cy_audit.sql'i içe aktar, çalıştır. Başka hiçbir kurulum adımı yok. Composer yok, npm yok, internet bağlantısı bile gerekmiyor — tüm kütüphaneler proje içinde.
Bu proje, Çılgın Yazılım Kütüphanesi altında yayınlanan açıklamalı, üretime hazır örneklerden biridir.
audit()Denetim kaydı, her işleyicinin kendi başına yazdığı bir şey olsaydı, yarın eklenen bir işleyici onu yazmayı unuturdu ve kimse fark etmezdi. Bu yüzden tek bir fonksiyon vardır:
audit($db, 'update', 'product', $id, $old, $new);
// ^ ^ ^ ^ ^ ^
// | | | | | └─ işlemden SONRAKİ değerler
// | | | | └─────── işlemden ÖNCEKİ değerler
// | | | └───────────── kayıt id'si
// | | └──────────────────────── mantıksal varlık türü
// | └────────────────────────────────── create | update | delete
// └──────────────────────────────────────── PDO bağlantısıINSERT bu fonksiyonun içindedir.
audit() herhangi bir sebeple patlarsa (bozuk kodlama, disk dolu, kilit zaman aşımı) ürün satırı çoktan kaydedilmiş olurdu ve ortada izi olmayan bir kayıt kalırdı. Bir denetim sisteminde bu, sessizce oluşan en tehlikeli durumdur: tabloya bakan kişi hiçbir eksik görmez.
$db->beginTransaction();
try {
$stmt->execute([...]); // veri
audit($db, 'create', 'product', $newId, null, $data); // kayıt
$db->commit(); // ya ikisi de…
} catch (PDOException $e) {
$db->rollBack(); // …ya hiçbiri
throw $e;
}system/ajax.php içinde audit_log tablosuna INSERT dışında bir işlem yapan tek bir uç nokta yoktur. Denetim satırı yalnızca bir CRUD işleminin yan etkisi olarak doğar; arayüzden silinemez, düzenlenemez.
Veritabanı seviyesinde de kilitlemek isterseniz, uygulama kullanıcısına o tabloda yalnızcaINSERTveSELECTyetkisi verin → Canlı Ortama Alırken
Arayüz
#islem-42)Altyapı
audit()password, token, cvv …)action tabanlı yönlendirmeAPP_DEBUG| Açık | Tipik hatalı kod | Bu projede |
|---|---|---|
| SQL Injection | "... WHERE id = '".$_POST['id']."'" | Tüm sorgular prepared statement, EMULATE_PREPARES = false. Sıralama sütunu beyaz listeden geçer; varlık türü filtresi AUDIT_ENTITIES sabit listesiyle doğrulanır; tarih filtreleri DateTime::createFromFormat ile biçim kontrolünden geçer. |
| XSS (denetim ekranında) | $('#cell').html(row.old_value) | Eski/yeni değerler doğrudan kullanıcı verisidir. Sunucuda e(), istemcide esc() ile kaçışlanır. Denetim ekranını genellikle en yetkili kullanıcılar açar — burada bir açık en pahalı açıktır. |
| CSRF | (genelde hiç yok) | Oturuma bağlı 32 baytlık token, her AJAX isteğinde, hash_equals() ile sabit zamanlı doğrulama. Token'sız istek → HTTP 403. |
| Denetim kaydının silinmesi | DELETE FROM logs WHERE id = ? | audit_log için hiçbir yazma uç noktası yok. Tek giriş yolu audit(). |
| İzi olmayan kayıt | Önce INSERT, sonra ayrı bir INSERT | Veri ve denetim kaydı tek transaction. Kayıt yazılamazsa veri de geri alınır. |
| Denetim kaydından sızıntı | old_values içinde düz parola | AUDIT_REDACT listesindeki alanlar loga yazılmadan önce *** ile değiştirilir. |
| Bozuk kodlamanın kaydı yutması | json_encode() → false → boş sütun | JSON_INVALID_UTF8_SUBSTITUTE: bozuk bayt U+FFFD olur, kayıt kaybolmaz. |
| CSV formül enjeksiyonu | Hücreyi olduğu gibi yazmak | =, +, -, @ ile başlayan hücrelerin başına tek tırnak konur; Excel formül olarak çalıştıramaz. |
| LIKE joker karakterleri | LIKE '%$search%' | %, _, \ kaçışlanır. |
| Kaynak tüketimi | LIMIT $length | Sayfa boyutu PAGE_SIZE_MAX (200), dışa aktarım EXPORT_MAX_ROWS (5000) ile sınırlı; yazma ve dışa aktarım ayrı hız sınırı kovalarında. |
| Bilgi sızıntısı | Ekrana basılan MySQL hataları | APP_DEBUG sunucu adından türetilir; canlıda otomatik false, detay error_log()'a gider. |
| Kurulum dosyasının indirilmesi | /cy_audit.sql → HTTP 200 | .htaccess: .sql, .md, .json, .log … kapalı (README dosyaları bilinçli istisnadır). |
Tüm ayarlar system/config.php içinde, açıklamalarıyla birlikte:
| Sabit | Varsayılan | Açıklama |
|---|---|---|
DB_HOST | 127.0.0.1 | Veritabanı sunucusu |
DB_NAME | cy_audit | Veritabanı adı |
DB_USER | root | Kullanıcı adı |
DB_PASS | (boş) | Parola |
AUDIT_REDACT | password, token, cvv … | Loga yazılmadan önce *** ile maskelenecek alan adları |
AUDIT_MODE | diff | diff → yalnızca değişen alanlar · full → her işlemde satırın tamamı |
AUDIT_ENTITIES | ['product' => 'Ürün'] | Denetlenebilen varlık türleri; filtre listesi ve doğrulama buradan beslenir |
PAGE_SIZE_MAX | 200 | DataTables sayfa boyutu tavanı |
EXPORT_MAX_ROWS | 5000 | CSV dışa aktarımda tek seferde okunacak en fazla satır |
RATE_LIMIT_WRITE | [60, 60] | Yazma işlemleri: 60 saniyede 60 istek |
RATE_LIMIT_EXPORT | [10, 60] | Dışa aktarım: 60 saniyede 10 istek |
APP_DEBUG | ortama göre | localhost / *.test / *.local → true, diğer her yerde false |
AUDIT_MODE: diff mi full mü?| | diff | full |
|-|--------|--------|
| Yazılan veri | Yalnızca değişen alanlar | Satırın tamamı |
| Tablo boyutu | Küçük | Büyür |
| "O an kayıt neydi?" | Zincirin tamamını okumak gerekir | Tek satırda cevap |
| Uygun olduğu yer | Sık güncellenen tablolar | Yasal saklama, geri alma (rollback) senaryoları |
Bu örnek diff kullanır. full'e geçmek tek satırlık bir değişikliktir; kod yolu ikisini de destekler.
Tüm DB_* sabitleri ortam değişkeniyle geçersiz kılınabilir:
# Linux / macOS
export DB_HOST=localhost DB_USER=uygulama DB_PASS='••••••••'
# Windows (PowerShell)
$env:DB_USER = "uygulama"; $env:DB_PASS = "••••••••".htaccess ya da httpd.conf içinde: SetEnv DB_PASS "guclu-sifre"
Denetim katmanını taşımak için iki dosya yeter: audit_log tablosu ve audit() fonksiyonu.
1 — Tabloyu oluşturun (cy_audit.sql içindeki CREATE TABLE audit_log bloğu)
2 — audit() ve yardımcılarını kopyalayın (system/function.php → BÖLÜM 5)
3 — Her yazma işleminden sonra çağırın:
// EKLEME — old yok
$db->beginTransaction();
$stmt->execute([...]);
$newId = (int) $db->lastInsertId();
audit($db, 'create', 'siparis', $newId, null, $data);
$db->commit();
// GÜNCELLEME — ÖNCE eski satırı okuyun, diff için şart
$old = $db->prepare('SELECT ... WHERE id = ?') /* ... */ ->fetch();
$db->beginTransaction();
$stmt->execute([...]);
audit($db, 'update', 'siparis', $id, $old, $data);
$db->commit();
// SİLME — new yok; old, kaydın son kopyasıdır
audit($db, 'delete', 'siparis', $id, $old, null);define('AUDIT_ENTITIES', ['product' => 'Ürün', 'siparis' => 'Sipariş']);FIELD_LABELS) — isteğe bağlıdır; eklemezseniz ham sütun adı gösterilir.
Aktör kim, nereden geliyor?
audit()aktörü$_SESSION['user_id']ve$_SESSION['user_name']üzerinden okur. Bu örnekte gerçek giriş olmadığı içinsystem/ajax.phpbaşında sahte bir aktör yazılır. Kendi projenizde o iki satırı silin; oturum zaten giriş sisteminizden gelecektir.
assets/css/cilginyazilim.css dosyası bu projeye değil markaya aittir. Diğer örnek projelerde de aynı görsel dili kullanabilmek için ayrı bir dosya olarak tutulur; bu örneğe özgü her şey (sayaç şeridi, işlem rozetleri, diff tablosu) assets/css/style.css içindedir.
| Sınıf | Ne işe yarar | ||
|---|---|---|---|
.cy-card / .cy-card__header / __body / __footer | Gradyan başlıklı ana kart | ||
.cy-brand / .cy-brand__mark / __title / __subtitle | Logo kutusu + başlık bloğu | ||
.cy-btn + --primary \ | --onbrand \ | --glass | Marka butonları |
.cy-btn-icon + --view \ | --edit \ | --delete | Tablo içi ikon butonları |
.cy-table / .cy-actions / .cy-id / .cy-name | Marka görünümlü tablo bileşenleri | ||
.cy-badge + --glass \ | --soft | Rozetler | |
.cy-modal / .cy-detail | Gradyan başlıklı modal ve etiket/değer listesi | ||
.cy-toast + --success \ | --danger \ | --info | Bildirim balonları |
Bu örneğe özgü (style.css): .cy-stats / .cy-stat, .cy-op--create|update|delete, .cy-diff / .cy-diff__old / .cy-diff__new, .cy-filters, .cy-stock, .cy-sku.
Tüm bileşenler CSS değişkenlerinden beslenir. Tek yeri değiştirmek yeter:
:root {
--cy-brand-900: #061321; /* Logodaki en koyu lacivert */
--cy-brand-600: #0b5cb5; /* Ana marka mavisi */
--cy-accent: #0ea5e9; /* Vurgu rengi */
--cy-gradient: linear-gradient(135deg, #061321, #0b5cb5 45%, #0284c7);
}
Kullanıcının işletim sistemi koyu temadaysa otomatik devreye girer. Zorlamak isterseniz:
<html data-cy-theme="dark"> <!-- veya "light" -->
.
├── index.php # Arayüz (sunum katmanı) — veritabanına dokunmaz
├── .env.example # Veritabanı bilgileri (isteğe bağlı) — .gitignore içinde
├── cy_audit.sql # Şema + 12 ürün + 8 örnek denetim kaydı
├── README.md / README.en.md # Belgelendirme
├── CHANGELOG.md # Sürüm notları
├── LICENSE # MIT
├── .htaccess # Güvenlik başlıkları, dosya erişim kuralları
├── .gitignore
│
├── docs/
│ └── screenshots/ # README'de kullanılan ekran görüntüleri
│
├── system/
│ ├── config.php # Ayarlar, oturum, PDO bağlantısı
│ ├── function.php # ★ audit() — tek yazma noktası + diff + maskeleme
│ ├── ajax.php # AJAX uç noktası / action yönlendiricisi
│ └── .htaccess # Bu klasöre doğrudan erişimi kısıtlar
│
└── assets/
├── css/
│ ├── bootstrap.min.css
│ ├── dataTables.bootstrap5.min.css
│ ├── cilginyazilim.css # ★ MARKA TASARIM KALIBI
│ └── style.css # Sadece bu sayfaya özel eklemeler
├── js/
│ ├── jquery-3.7.0.js
│ ├── bootstrap.bundle.js
│ ├── jquery.dataTables.min.js
│ ├── dataTables.bootstrap5.min.js
│ └── audit.js # ★ Arayüz mantığı
└── images/logo.pngCSS: bootstrap → dataTables → cilginyazilim → style
JS: jQuery → bootstrap.bundle → dataTables → dataTables.bootstrap5 → audit
┌─────────────────────────────────────────────────────────────────────┐
│ TARAYICI (index.php + assets/js/audit.js) │
│ │
│ Ürün tablosu ──┐ │
│ Form gönder ───┤ │
│ Filtreler ─────┼──► jQuery AJAX ──► POST { action, csrf_token, … } │
│ Geçmiş tablosu ┘ │ │
│ │ │
│ Yazma bitince refreshAll(): │ │
│ ürün tablosu + geçmiş tablosu + sayaçlar │ │
└──────────────────────────────────────────────┼───────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────┐
│ SUNUCU (system/ajax.php) │
│ │
│ 1. POST mu? → değilse 405 │
│ 2. require_csrf() → geçersizse 403 │
│ 3. rate_limit() → aşılırsa 429 │
│ 4. action'a göre dağıt │
│ 5. validate_product() → hatalıysa 422 + errors │
│ │
│ 6. ┌── BEGIN TRANSACTION ───────────────────────────────┐ │
│ │ INSERT / UPDATE / DELETE → products │ │
│ │ audit() → audit_log │ │
│ │ ├─ diff_values() : yalnızca değişen alanlar │ │
│ │ ├─ redact() : hassas alanlar → '***' │ │
│ │ └─ audit_json() : bozuk bayt → U+FFFD │ │
│ └── COMMIT (biri patlarsa ROLLBACK: ikisi de yok) ┘ │
│ │
│ 7. json_response() → tek noktadan JSON çıkışı │
└─────────────────────────────────────────────────────────────────────┘
| Dosya | Görevi |
|---|---|
| index.php | Sadece sunum. CSRF token üretir, tabloları/modalları çizer. Veritabanına dokunmaz. |
| assets/js/audit.js | DataTables kurulumu, filtreler, modallar, diff çizimi, CSV formu. |
| system/ajax.php | Yönlendirici + tüm güvenlik kontrolleri + transaction sınırları. |
| system/function.php | audit(), diff_values(), redact(), CSRF, hız sınırı, CSV yardımcıları. |
| system/config.php | Oturum, sabitler, tek bir PDO örneği. |
Tüm istekler POST ile system/ajax.php adresine yapılır ve geçerli bir csrf_token içermelidir.
action=product_list — Ürün listesi (DataTables)
İstek: draw, start, length, search[value], order[0][column], order[0][dir]
Sıralanabilir sütunlar beyaz listeyle sınırlıdır: 0 → id, 1 → name, 2 → sku, 3 → price, 4 → stock, 5 → updated_at. İşlem sütunu (6) veritabanı sütunu olmadığı için sıralanamaz.
action=product_save — Ekleme / güncelleme
İstek: id (0 veya boş → ekleme), name, sku, price, stock
Başarılı (200):
{ "success": true, "type": "success", "description": "Ürün güncellendi (2 alan).", "id": 5 }diff_values() çıktısından gelir. Hiçbir alan değişmediyse "Değişiklik yapılmadı." döner ve denetim kaydı yazılmaz.
Doğrulama hatası (422):
{
"success": false, "type": "danger",
"description": "Lütfen formdaki hataları düzeltin.",
"errors": { "sku": "SKU 2-40 karakter; büyük harf, rakam ve tire." }
}errors anahtarları form alanlarının name değerleriyle birebir aynıdır. SKU çakışmasında 409 döner, gövde yine errors.sku taşır.
action=product_fetch — Tek ürün (form doldurma)
İstek: id — Hazır HTML değil ham veri döner:
{ "success": true, "product": { "id": 5, "name": "Laptop Soğutucu Stand", "sku": "STND-COOL", "price": "429.90", "stock": 58 } }
action=product_delete — Silme
İstek: id — Ürün silinir, delete denetim kaydı yazılır. old_values kaydın son hâlinin tamamını taşır; bu, silinen kaydın tek kopyasıdır.
action=audit_list — Denetim listesi (salt-okunur)
İstek: DataTables parametreleri + f_action, f_entity, f_from, f_to
Filtreler audit_filters() içinde tek yerden kurulur; audit_export aynı fonksiyonu kullanır, böylece ekrandaki liste ile inen dosya asla ayrışmaz.
action=audit_detail — Alan bazlı diff
İstek: id
{
"success": true,
"meta": { "id": 5, "time": "27.08.2026 13:20:38", "actor": "Mehmet YILMAZ",
"action": "update", "action_tr": "Güncellendi", "entity": "Ürün #5",
"ip": "10.0.0.12", "user_agent": "Mozilla/5.0 …" },
"diff": [
{ "field": "price", "label": "Fiyat", "old": "379.90", "new": "429.90" },
{ "field": "stock", "label": "Stok adedi", "old": 95, "new": 58 }
]
}field ham sütun adıdır (veritabanına yazılan), label ekranda gösterilen Türkçe etikettir. null = o alan bu işlemde yoktu (create'te old, delete'te new).
action=audit_export — CSV dışa aktarım
İstek: f_action, f_entity, f_from, f_to, search_value
text/csv döner (Content-Disposition: attachment). UTF-8 BOM'lu, ; ayraçlı — Türkçe Excel'de doğrudan açılır. Her değişen alan ayrı satırdır, böylece dosya Excel'de süzülebilir.
action=stats — Sayaç şeridi
{ "success": true, "products": 12, "total": 8, "create": 3, "update": 4, "delete": 1, "last": "30.08.2026 07:20:38" }SUM(action = '…')), tablo bir kez taranır.
| Kod | Anlamı |
|---|---|
200 | İşlem başarılı |
400 | Geçersiz parametre (örn. hatalı ID) |
403 | CSRF token geçersiz veya oturum düşmüş |
404 | Kayıt bulunamadı |
405 | POST dışı istek |
409 | Benzersizlik çakışması (SKU zaten var) |
422 | Form doğrulama hatası (errors alanı döner) |
429 | Hız sınırı aşıldı (retry_after saniye döner) |
500 | Sunucu / veritabanı hatası |
CREATE TABLE `audit_log` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`actor_id` INT UNSIGNED NULL DEFAULT NULL,
`actor_name` VARCHAR(150) NOT NULL DEFAULT 'Sistem',
`action` ENUM('create','update','delete') NOT NULL,
`entity_type` VARCHAR(60) NOT NULL, -- 'product', 'siparis' …
`entity_id` BIGINT UNSIGNED NOT NULL,
`old_values` JSON NULL, -- create'te NULL
`new_values` JSON NULL, -- delete'te NULL
`ip` VARCHAR(45) NOT NULL DEFAULT '',
`user_agent` VARCHAR(255) NOT NULL DEFAULT '',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_audit_entity` (`entity_type`, `entity_id`),
KEY `idx_audit_action` (`action`),
KEY `idx_audit_actor` (`actor_id`),
KEY `idx_audit_created` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
| Karar | Neden |
|---|---|
BIGINT id | Denetim tablosu ana tablodan kat kat hızlı büyür; INT tavanı (2,1 milyar) düşünüldüğünden erken gelir |
entity_type + entity_id | Tek tablo tüm varlıkları denetler. Her varlık için ayrı log tablosu açmak, "bu kullanıcı bugün ne yaptı?" sorusunu UNION cehennemine çevirir |
JSON sütunlar | Alan sayısı varlıktan varlığa değişir. Sabit sütunlar (old_price, old_stock …) yeni bir alan eklendiğinde şema değişikliği gerektirirdi |
actor_id ve actor_name | İkisi birlikte tutulur: kullanıcı silinse veya adı değişse bile o günkü ad kayıtta kalır |
idx_audit_created | Tarih aralığı filtresinin dayandığı indeks; olmadan her sorgu tam tarama olur |
| InnoDB | Transaction desteği — veri ve kaydın birlikte yazılabilmesinin ön şartı |
Denetim tablosu ne kadar büyür? Ne yapmalıyım?
diff modunda bir güncelleme satırı tipik olarak 150–400 bayt tutar. Günde 10.000 işlem yapan bir sistemde yıllık ~1 GB eder. İki yaygın strateji:
created_at üzerinden aylık RANGE bölümleri; eski bölümü tek komutla düşürürsünüz.Neden logs tablosuna cümle yazmıyoruz?
"Ahmet ürünü güncelledi" cümlesi üç soruyu da cevapsız bırakır: hangi alan, neydi, ne oldu? Ayrıca aranamaz, süzülemez ve raporlanamaz. Alan bazlı diff ise "price alanı 429.90'dan 599.90'a çekilmiş" sorusunu SQL ile cevaplayabilir hâle getirir.
audit_log tablosunu veritabanı seviyesinde de kilitleyebilir miyim?
Evet, önerilir. Uygulama kullanıcısına o tabloda yalnızca ekleme ve okuma yetkisi verin:
REVOKE UPDATE, DELETE ON cy_audit.audit_log FROM 'uygulama'@'localhost';
GRANT INSERT, SELECT ON cy_audit.audit_log TO 'uygulama'@'localhost';Hassas alanları maskelemek yerine hiç yazmasam olmaz mı?
Olur ama bilgi kaybedersiniz. *** yazmak "bu alan değişti ama içeriğini saklamıyoruz" der; hiç yazmamak ise "bu alan hiç değişmedi" ile karışır. Denetimde bu ikisi çok farklı iki cümledir.
Denetim kaydını başka bir veritabanına yazabilir miyim?
Yazabilirsiniz ama transaction garantisini kaybedersiniz — bu projedeki en önemli özelliklerden biri budur. Ayrı sunucu gerekiyorsa önce yerel tabloya yazın, sonra bir kuyruk işçisiyle kopyalayın.
Neden hem istemcide hem sunucuda doğrulama var?
İstemci doğrulaması kullanıcı deneyimi içindir; JavaScript kapatılabilir veya istek doğrudan curl ile atılabilir. Gerçek koruma her zaman sunucudadır.
serverSide: true ayarını kapatabilir miyim?
Ürün tablosunda kapatabilirsiniz. Denetim tablosunda kapatmayın: o tablo hızla yüz binlerce satıra ulaşır ve hepsini tarayıcıya indirmek sayfayı dondurur.
APP_DEBUG zaten ortamdan türetiliyor — yine de canlıda false olduğunu doğrulayınroot yerine sınırlı yetkili bir kullanıcı oluşturunaudit_log tablosunda uygulama kullanıcısından UPDATE ve DELETE yetkisini alınsession.cookie_secure = 1 ve session.cookie_httponly = 1 ayarlayınsystem/ajax.php başındaki sahte aktör satırlarını silin.htaccess çalışmaz; .sql ve .md erişimini sunucu yapılandırmasından kapatın:```nginx
location ~* \.(sql|log|ini|bak)$ { deny all; }
```
audit_log yedeklerini ana veriyle aynı sıklıkta alın| Belirti | Çözüm |
|---|---|
| "Veritabanına bağlanılamadı" | MySQL çalışmıyor veya DB_* bilgileri hatalı. XAMPP panelinden MySQL'i başlatın. |
CONSTRAINT ... failed (JSON sütunu) | Eski bir sürümde json_encode() bozuk UTF-8'de false dönüyordu. Bu sürümde audit_json() bunu kapatır; güncel dosyayı kullandığınızdan emin olun. |
| Tablo boş, "Yükleniyor…" takılı | F12 → Network. Genelde system/ajax.php bir PHP hatası döndürüyordur; yerelde APP_DEBUG zaten true, yanıtı okuyun. |
| HTTP 403 dönüyor | Oturum düşmüş — sayfayı yenileyin. Sunucuda session.save_path yazılabilir olmalıdır. |
| CSV Excel'de bozuk görünüyor | Dosya UTF-8 BOM'lu ve ; ayraçlıdır. Excel "Metni Sütunlara Dönüştür" ile açtıysanız ayracı ; seçin. |
$ is not defined | JavaScript yükleme sırası bozulmuş. jQuery her zaman en başta gelmelidir. |
| Türkçe karakterler bozuk | Veritabanı utf8mb4 değil. cy_audit.sql sonundaki CONVERT TO CHARACTER SET utf8mb4 satırlarını çalıştırın. |
| DataTables "Requested unknown parameter" | <th> sayısı ile sunucudan dönen dizi uzunluğu farklı. İkisini eşitleyin. |
| Geçmişe satır düşmüyor | Güncellemede hiçbir alan değişmemiş olabilir — bu durumda kayıt bilinçli olarak yazılmaz. |
old_values ile kaydı eski hâline döndürmefull modda satır anlık görüntülerinden zaman çizelgesi görünümüdiff_values, redact, valid_date)Bu proje herkese açıktır — dilediğiniz geliştirmeyle katkı sağlayabilirsiniz.
📦 Depo: github.com/CilginYazilim/audit-log
| Nasıl katkı sağlarım? | Nereden |
|---|---|
| 🐛 Hata bildir | Issues |
| 💡 Özellik öner | Issues |
| 🔧 Kod gönder | Pull Requests |
| ❓ Soru sor | Discussions |
audit_log tablosuna yazma uç noktası eklemeyin. Append-only olması projenin tezidir.audit() çağrısını ve transaction'ı unutmayın.style.css üzerinden yapın; cilginyazilim.css markaya aittir ve diğer projelerle ortaktır.MIT — ticari kullanım dahil serbesttir.
cilginyazilim.com tarafından ❤ ile geliştirildi
Faydalı bulduysanız ⭐ vermeyi unutmayın.
İncelemek istediğiniz dosyayı ağaçtan seçin.
Güvenlik gereği kaynak dosyalardaki parola, API anahtarı ve benzeri gizli değerler gösterilmeden önce maskelenir (••••••••).
"Ahmet ürünü güncelledi" cümlesi üç soruyu da cevapsız bırakır: hangi alan değişti, neydi, ne oldu? Üstelik aranamaz, süzülemez ve raporlanamaz. Alan bazlı diff ise "geçen ay hangi fiyatlar yükseltildi?" sorusunu SQL ile cevaplanabilir hâle getirir. Bu örnek değişen her alanın eski ve yeni değerini JSON sütunlarda saklar.
Uygulama tarafında audit_log tablosuna INSERT dışında işlem yapan hiçbir uç nokta yoktur; denetim satırı yalnızca bir CRUD işleminin yan etkisi olarak doğar. Veritabanı tarafında da kilitlemek için uygulama kullanıcısından o tablodaki UPDATE ve DELETE yetkisini almanız yeterlidir. O zaman bir SQL enjeksiyonu açığı bile denetim kaydını değiştiremez.
Hiçbir şey kaydedilmez. Veri ve denetim kaydı aynı transaction içinde yazılır: biri başarısız olursa ikisi de geri alınır. Bu ayrıntı önemlidir, çünkü aksi hâlde ortada izi olmayan bir kayıt kalırdı ve tabloya bakan kişi hiçbir eksik göremezdi. Bir denetim sisteminde en tehlikeli hata, sessiz olanıdır.
Hayır. Yapılandırmadaki listede yer alan alanların (password, token, cvv gibi) değeri, loga yazılmadan önce yıldızla değiştirilir. Alanın kendisi kayıtta görünmeye devam eder; yani "bu alan değişti ama içeriğini saklamıyoruz" bilgisi korunur. Hiç yazmamak ise "bu alan hiç değişmedi" ile karışırdı.
Denetim tablosu, denetlediği tablodan kat kat hızlı büyür. İki yaygın yol vardır: tarih sütunu üzerinden aylık bölümleme (partitioning) ya da belirli bir yaştan eski satırları soğuk bir tabloya arşivleme. Silmek yerine arşivleyin; denetim kaydını silmek, onu tutma sebebini zayıflatır.
İki şey taşımanız yeterli: audit_log tablosu ve audit() fonksiyonu. Ardından her yazma işleminden sonra audit() çağrısını transaction içine koyarsınız. Güncellemede eski satırı işlemden ÖNCE okumanız gerekir; diff onun üzerinden üretilir. Yeni varlık türünü yapılandırmadaki listeye eklediğinizde filtreler ve doğrulama kendiliğinden çalışır.
Bu örneğin daha gelişmişi projenizde mi lazım?
İhtiyacınızı anlatın; bu yapıyı sizin sisteminize uyarlayalım ya da size özel geliştirelim.
Henüz yorum yok. Takıldığınız bir yer varsa ilk soruyu siz sorun.
Yorumunuz onaylandıktan sonra yayınlanır. Teknik sorularınıza ekibimiz yanıt verir.
İleri
guvenlik/guvenli-dosya-yukleme
195 inceleme 75 canlı açılış 1 beğeni
İleri
guvenlik/rol-tabanli-yetkilendirme-paneli
180 inceleme 70 canlı açılış 1 beğeni
İleri
guvenlik/php-rest-api-panel
172 inceleme 60 canlı açılış 1 beğeni