İşlem Geçmişi (Audit Log)

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.

PHP 8 PDO MySQL Ajax DataTables Bootstrap 5
Seviye
Orta
Dosya
26
Kod satırı
~12.190
Proje boyutu
1.8 MB
Veritabanı
cy_audit
Lisans
MIT
İnceleme
92
Beğeni
1
Yayın

Ekran Görüntüleri 3 görsel

Bu Örnek Ne Yapıyor?

  • audit() tek yazma noktası: diff üretimi, maskeleme ve INSERT tek fonksiyonda toplanmış
  • Alan bazlı diff: yalnızca değişen alanlar saklanır, hiçbir alan değişmediyse satır hiç yazılmaz
  • Append-only: audit_log tablosuna INSERT dışında işlem yapan hiçbir uç nokta yok
  • Veri ve denetim kaydı aynı transaction'da yazılır; biri başarısız olursa ikisi de geri alınır
  • Hassas alanlar (password, token, cvv) loga yazılmadan ÖNCE *** ile maskelenir
  • İşlem türü, varlık türü ve tarih aralığı filtreleri; ekrandaki filtreyle birebir aynı CSV dışa aktarım
  • Paylaşılabilir kayıt bağlantısı (#islem-42) ve alan bazlı renkli diff modalı
  • Mobilde yatay kaydırma yok: ikincil sütunlar gizlenir, bilgi detay modalında korunur
  • Kodun her satırı Türkçe açıklamalı; Composer, npm ve internet bağlantısı gerekmiyor

Nerede İşe Yarar?

  • ERP, muhasebe ve yönetim panellerine "bu kaydı kim, ne zaman değiştirdi?" cevabı veren bir katman eklemek
  • Fiyat, stok ya da yetki gibi hassas alanların geçmişini denetlenebilir biçimde tutmak
  • Yasal saklama veya iç denetim gereği değişiklik kaydı tutmak zorunda olan projeler
  • Var olan bir "logs" tablosunu alan bazlı, silinemez bir denetim kaydına yükseltmek
  • PHP'de transaction, JSON sütunları ve sunucu taraflı DataTables kullanımını örnek üzerinden öğrenmek

Gereksinimler

  • PHP 8.0+ (pdo_mysql) · MySQL 5.7+ veya MariaDB 10.3+ (JSON sütunu) · Apache/mod_rewrite

Nasıl Kurulur?

  1. Proje dosyalarını web köküne kopyalayın (XAMPP'ta htdocs ya da kendi sunucunuzun kök dizini)
  2. cy_audit.sql dosyasını içe aktarın — veritabanını, tabloları ve 12 ürün ile 8 örnek denetim kaydını kendisi oluşturur
  3. Canlı sunucuda system/config.local.php.example dosyasını config.local.php adıyla kopyalayıp veritabanı künyenizi yazın (yerelde gerek yok)
  4. Uygulamayı açın; bir ürünün fiyatını değiştirip kaydedin ve alttaki geçmişe düşen satırı inceleyin
  5. Göz butonuna basarak alan bazlı diff'i, ardından CSV butonuyla dışa aktarımı deneyin

Veritabanı şeması projedeki cy_audit.sql dosyasında.

Nasıl Çalışıyor?

Çılgın Yazılım

İşlem Geçmişi (Audit Log)

PHP PDO · MySQL · AJAX · DataTables · Bootstrap 5 · Çılgın Yazılım Tasarım Kalıbı

Her değişikliğin izini bırakan, kendisi değiştirilemeyen kayıt.

PHP
MySQL
Bootstrap
DataTables
License

🇹🇷 Türkçe · 🇬🇧 English

▶ Canlı Demo · Kaynak Kütüphanesi · cilginyazilim.com


Canlı Demo

Kurulum yok, kayıt yok, indirme yok — tarayıcınızdan 3 saniyede deneyin.

Canlı Demoyu Aç
Kaynak Kodu İncele
ZIP İndir

İşlem geçmişi canlı demo önizlemesi

▲ Görsele tıklayarak demoyu açabilirsiniz

Demoda 60 saniyede neleri deneyebilirsiniz?

#Şunu deneyinPerde arkasında ne oluyor?
1Bir ürünün fiyatını değiştirip kaydedinAlttaki geçmişe anında Güncellendi satırı düşer; toast "1 alan" der — bu sayı tahmin değil, sunucudaki diff_values() çıktısıdır
2Aynı ürünü hiçbir şey değiştirmeden tekrar kaydedinGeçmişe satır eklenmez. "Kaydet'e bastım" bir olay değildir; denetim kaydı yalnızca gerçek değişikliği tutar
3👁 Göz butonuna basınAlan bazlı diff açılır: eski değer üstü çizili kırmızı, yeni değer yeşil. Değişmeyen alanlar listede hiç yoktur
4Detay modalı açıkken adres çubuğuna bakın#islem-42 yazar. Bağlantıyı kopyalayıp gönderdiğinizde karşı taraf doğrudan o kaydı açar
5Bir ürünü silinÜrün gider, izi kalır: delete satırının old_values alanı kaydın son hâlinin tamamını taşır
6Ürün adına ` yazmayı deneyinKayıt reddedilir; kaydedilse bile hem listeye hem diff hücrelerine kaçışlanarak basılırdı
7İşlem türü filtresini Silindi yapınFiltre sunucuda uygulanır ve DataTables'ın kendi arama kutusuyla birlikte çalışır
8Tarih aralığı seçinBitiş günü tamamen dahildir (< ertesi gün`); klasik "son günün kayıtları kayboldu" hatası yoktur
9CSV butonuna basınEkrandaki aynı filtrelerle dosya iner. Her değişen alan ayrı satırdır; Excel'de süzülebilir
10Telefonunuzdan açınTablo yatay kaydırmaya zorlamaz; ikincil sütunlar gizlenir, bilgi detay modalında durur
İpucu: Demoyu açıkken F12 → Network sekmesini açın. Her istekte ajax.php'ye giden action ve csrf_token alanlarını, dönen JSON'u ve HTTP durum kodlarını (200 / 403 / 422 / 429) canlı görebilirsiniz.

Demo alanı hakkında bilinmesi gerekenler

KonuDurum
Verilercy_audit.sql içindeki 12 ürün + 8 örnek denetim kaydı. Gerçek kişi verisi yoktur.
SıfırlamaDemo veritabanı düzenli aralıklarla başlangıç haline döner; eklediğiniz kayıtlar kalıcı değildir.
Kimlik doğrulamaYoktur. Bilinçli bir tercihtir — örnek, denetim katmanına odaklanır. Aktör adı oturuma sahte olarak yazılır (bkz. Aktör nereden geliyor?).
APP_DEBUGCanlıda otomatik false — sunucu adından türetilir, yerelde true kalır.
BağımlılıkSıfır. Composer yok, npm yok, CDN yok. Demo internetsiz bir sunucuda da aynı çalışır.
Demo geçici olarak kapalıysa endişelenmeyin: depoyu klonlayıp cy_audit.sql'i içe aktarmanız aynı ekranı kendi bilgisayarınızda 2 dakikada ayağa kaldırır → Kurulum

Bu Proje Nedir?

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?

  • Kendi projesine denetim / işlem geçmişi katmanı ekleyecekler
  • "Bu kaydı kim değiştirdi?" sorusunu üretimde cevaplamak zorunda kalanlar
  • PHP + AJAX + DataTables üçlüsünü doğru öğrenmek isteyenler
  • Bootstrap 5 üzerine kurulu, tekrar kullanılabilir bir tasarım kalıbı arayanlar
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.


İçindekiler


Ekran Görüntüleri

Genel görünüm

Üstte yazılabilir ürün tablosu, altta salt-okunur denetim kaydı. Aradaki sayaç şeridi denetim kaydının nabzını gösterir: toplam kayıt, ekleme / güncelleme / silme sayıları ve son işlem zamanı.

Genel görünüm

Alan bazlı diff detayı

Yalnızca değişen alan listelenir. Eski değer üstü çizili kırmızı, yeni değer vurgulu yeşil. Alan adı hem Türkçe etiketiyle hem ham sütun adıyla (price) gösterilir — çünkü veritabanına yazılan her zaman ham addır.

Diff detayı

Mobil görünüm

Dar ekranda tablo yatay kaydırmaya zorlamaz: ikincil sütunlar gizlenir, bilgi detay modalında korunur. Dokunma hedefleri en az 32–44px'dir.

Mobil görünüm

Üç modal:

ModalAçılışİçerik
Ekle / DüzenleYeni Ürün veya kalem butonuForm, alan bazlı hata mesajları, "bu işlem geçmişe düşecek" uyarısı
🗑 Silme onayıÇöp kutusu butonuNeyin silineceği + "denetime kaydedilir" uyarısı
👁 İşlem detayıGeçmiş tablosundaki göz butonuMeta bilgiler (zaman, aktör, IP, tarayıcı) + alan bazlı diff tablosu + paylaşılabilir #islem-42 bağlantısı

Üç Kritik Karar

1) Tek yazma noktası — 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ı

Yeni bir varlık denetlemek istediğinizde yazacağınız kod tek satırdır. Diff üretimi, maskeleme ve INSERT bu fonksiyonun içindedir.

2) Veri ve kaydı aynı transaction'da yazmak

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;
}

InnoDB kullanmamızın somut karşılığı budur.

3) Append-only — kaydın kendisi de korunmalı

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ızca INSERT ve SELECT yetkisi verin → Canlı Ortama Alırken

Neler Var?

Arayüz

  • Marka gradyanlı başlık ve modallar
  • Sayaç şeridi (toplam / ekleme / güncelleme / silme / son işlem)
  • Üç modal (form / silme onayı / işlem detayı)
  • Alan bazlı renkli diff tablosu
  • İşlem türü, varlık türü ve tarih aralığı filtreleri
  • CSV dışa aktarım (ekrandaki filtrelerle birebir aynı)
  • Paylaşılabilir kayıt bağlantısı (#islem-42)
  • Sağ üstte toast bildirimleri
  • Otomatik koyu tema (işletim sistemi ayarını izler)
  • Mobil için ayrıca inceltilmiş — yatay kaydırma yok, dokunma hedefleri ≥32px, ARIA etiketli
  • Tamamı Türkçe — CDN'siz, çevrimdışı çalışır

Altyapı

  • Tek yazma noktası: audit()
  • Alan bazlı diff (yalnızca değişen alanlar)
  • Hassas alan maskeleme (password, token, cvv …)
  • Append-only denetim tablosu
  • Veri + kayıt aynı transaction'da
  • Sunucu taraflı (server-side) DataTables
  • Tek AJAX uç noktası, action tabanlı yönlendirme
  • CSRF + hız sınırı (yazma ve dışa aktarım ayrı)
  • Alan bazlı hata mesajları (HTTP 422)
  • Ortam değişkeni desteği, ortama göre otomatik APP_DEBUG
  • Kodun her satırı açıklamalı

Güvenlik: Neyi, Nasıl Kapattık?

AçıkTipik hatalı kodBu 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 silinmesiDELETE 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 INSERTVeri 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 parolaAUDIT_REDACT listesindeki alanlar loga yazılmadan önce * ile değiştirilir.
Bozuk kodlamanın kaydı yutmasıjson_encode()false → boş sütunJSON_INVALID_UTF8_SUBSTITUTE: bozuk bayt U+FFFD olur, kayıt kaybolmaz.
CSV formül enjeksiyonuHü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 karakterleriLIKE '%$search%'%, _, \ kaçışlanır.
Kaynak tüketimiLIMIT $lengthSayfa 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).

Kurulum

Sadece görmek istiyorsanız kurulum gerekmez → Canlı Demoyu açın. Aşağıdaki adımlar projeyi kendi bilgisayarınızda çalıştırmak içindir (~2 dakika).

Gereksinimler

  • PHP 8.0+ (pdo_mysql eklentisi)
  • MySQL 5.7+ veya MariaDB 10.3+ (JSON sütunu için)
  • Apache (XAMPP / WAMP / Laragon) — ya da PHP'nin yerleşik sunucusu

Adımlar

1 — Projeyi indirin

git clone https://github.com/CilginYazilim/audit-log.git
cd audit-log

2 — Veritabanını oluşturun

cy_audit.sql veritabanını da kendisi oluşturur; önceden cy_audit adında bir veritabanı açmanıza gerek yok.

mysql -u root -p < cy_audit.sql
İsteğe bağlı — kendi veritabanı bilgileriniz:
cp .env.example .env (Windows: copy .env.example .env) deyip DB_*
satırlarını doldurun. Bu dosya olmadan da çalışır; varsayılanlar yerel bir
XAMPP kurulumuna (root, boş parola) göredir. .env .gitignore
içindedir — parolanız depoya gitmez.

phpMyAdmin ile: İçe Aktar → Dosya seç → cy_audit.sql → Başlat

3 — Çalıştırın

php -S 127.0.0.1:8000

XAMPP kullanıyorsanız projeyi htdocs altına koyup şu adresi açın:
http://localhost/audit-log/

4 — Tarayıcıda açınhttp://127.0.0.1:8000/

Karşınıza 12 ürün ve 8 örnek denetim kaydı dolu, çalışır durumda bir ekran gelecek.

Ortam değişkenleri

Depo kökündeki .env dosyasına yazın; system/config.php dosyasına
hiç dokunmayın:

cp .env.example .env        # Windows: copy .env.example .env

.env .gitignore içindedir: depoya gönderilmez ve dağıtım (deploy) onu
silmez. system/config.php ise depoda durur ve her dağıtımda depodaki
sürümle değiştirilir — parolayı oraya yazarsanız hem GitHub'a gider hem de
ilk deploy'da kaybolur.

Dosyayı hiç oluşturmasanız da uygulama çalışır; aşağıdaki varsayılanlar
yerel bir XAMPP kurulumuna göredir.

Değer arama sırası: .env → sunucunun gerçek ortam değişkeni
(Apache SetEnv, systemd…) → buradaki varsayılan.

DeğişkenVarsayılanNe işe yarar
DB_HOST127.0.0.1Veritabanı sunucusu
DB_NAMEcy_auditVeritabanı adı
DB_USERrootKullanıcı
DB_PASS(boş)Şifre — koda yazmayın
APP_TIMEZONEEurope/IstanbulPHP'nin saat dilimi
APP_DEBUGortamdanHataların ekrana basılıp basılmayacağı

APP_TIMEZONE neden var? XAMPP'ın php.ini dosyasındaki
date.timezone, MySQL'in kullandığı sistem diliminden farklı olabilir.
Test makinesinde PHP Europe/Berlin, MySQL Europe/Istanbul
kullanıyordu; aynı anı anlatan iki satır bir saat farklı görünüyordu.
Zaman hesapları SQL tarafında yapıldığı için doğruydu, ama ekrana
basılan saat kayıyordu. Artık dilim açıkça sabitleniyor — sunucunuz başka
bir bölgedeyse bu değişkeni tanımlamanız yeterli, koda dokunmayın.


Yapılandırma

Tüm ayarlar system/config.php içinde, açıklamalarıyla birlikte:

SabitVarsayılanAçıklama
DB_HOST127.0.0.1Veritabanı sunucusu
DB_NAMEcy_auditVeritabanı adı
DB_USERrootKullanıcı adı
DB_PASS(boş)Parola
AUDIT_REDACTpassword, token, cvvLoga yazılmadan önce *** ile maskelenecek alan adları
AUDIT_MODEdiffdiff → 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_MAX200DataTables sayfa boyutu tavanı
EXPORT_MAX_ROWS5000CSV 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_DEBUGortama görelocalhost / .test / .localtrue, 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.

Şifreyi koda yazmayın

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 = "••••••••"

Apache için .htaccess ya da httpd.conf içinde: SetEnv DB_PASS "guclu-sifre"


Kendi Projenize Eklemek

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);

4 — Yeni varlığı tanıtın (system/config.php):

define('AUDIT_ENTITIES', ['product' => 'Ürün', 'siparis' => 'Sipariş']);

5 — Alan etiketlerini ekleyin (system/function.phpFIELD_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çin system/ajax.php başında sahte bir aktör yazılır. Kendi projenizde o iki satırı silin; oturum zaten giriş sisteminizden gelecektir.

Çılgın Yazılım Tasarım Kalıbı

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.

Hazır bileşenler

SınıfNe işe yarar
.cy-card / .cy-card__header / __body / __footerGradyan başlıklı ana kart
.cy-brand / .cy-brand__mark / __title / __subtitleLogo kutusu + başlık bloğu
.cy-btn + --primary \--onbrand \--glassMarka butonları
.cy-btn-icon + --view \--edit \--deleteTablo içi ikon butonları
.cy-table / .cy-actions / .cy-id / .cy-nameMarka görünümlü tablo bileşenleri
.cy-badge + --glass \--softRozetler
.cy-modal / .cy-detailGradyan başlıklı modal ve etiket/değer listesi
.cy-toast + --success \--danger \--infoBildirim 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.

Renkleri değiştirmek

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);
}

Koyu tema

Kullanıcının işletim sistemi koyu temadaysa otomatik devreye girer. Zorlamak isterseniz:

<html data-cy-theme="dark">   <!-- veya "light" -->

Dosya Yapısı

.
├── 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.png

Yükleme sırası önemlidir:

CSS:  bootstrap → dataTables → cilginyazilim → style
JS:   jQuery → bootstrap.bundle → dataTables → dataTables.bootstrap5 → audit

Nasıl Çalışıyor?

┌─────────────────────────────────────────────────────────────────────┐
│  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ışı         │
└─────────────────────────────────────────────────────────────────────┘

Sorumluluk dağılımı

DosyaGörevi
index.phpSadece sunum. CSRF token üretir, tabloları/modalları çizer. Veritabanına dokunmaz.
assets/js/audit.jsDataTables kurulumu, filtreler, modallar, diff çizimi, CSV formu.
system/ajax.phpYönlendirici + tüm güvenlik kontrolleri + transaction sınırları.
system/function.phpaudit(), diff_values(), redact(), CSRF, hız sınırı, CSV yardımcıları.
system/config.phpOturum, sabitler, tek bir PDO örneği.

AJAX API Referansı

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 }

Mesajdaki alan sayısı 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" }

Dört sayaç tek sorguda üretilir (SUM(action = '…')), tablo bir kez taranır.

HTTP durum kodları

KodAnlamı
200İşlem başarılı
400Geçersiz parametre (örn. hatalı ID)
403CSRF token geçersiz veya oturum düşmüş
404Kayıt bulunamadı
405POST dışı istek
409Benzersizlik çakışması (SKU zaten var)
422Form doğrulama hatası (errors alanı döner)
429Hız sınırı aşıldı (retry_after saniye döner)
500Sunucu / veritabanı hatası

Veritabanı Şeması

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;
KararNeden
BIGINT idDenetim tablosu ana tablodan kat kat hızlı büyür; INT tavanı (2,1 milyar) düşünüldüğünden erken gelir
entity_type + entity_idTek 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ütunlarAlan 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_createdTarih aralığı filtresinin dayandığı indeks; olmadan her sorgu tam tarama olur
InnoDBTransaction desteği — veri ve kaydın birlikte yazılabilmesinin ön şartı

Sık Sorulanlar

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:

  1. Bölümleme (partitioning): created_at üzerinden aylık RANGE bölümleri; eski bölümü tek komutla düşürürsünüz.
  2. Arşivleme: N aydan eski satırları soğuk bir tabloya/dosyaya taşıyın. Silmeyin — arşivleyin; denetim kaydını silmek, tuttuğunuz kaydın anlamını zayıflatır.

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';

Böylece bir SQL injection açığı bile denetim kaydını değiştiremez.

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.


Canlı Ortama Alırken

  • [ ] APP_DEBUG zaten ortamdan türetiliyor — yine de canlıda false olduğunu doğrulayın
  • [ ] Veritabanı için root yerine sınırlı yetkili bir kullanıcı oluşturun
  • [ ] audit_log tablosunda uygulama kullanıcısından UPDATE ve DELETE yetkisini alın
  • [ ] Kimlik bilgilerini ortam değişkeni olarak tanımlayın, koda gömmeyin
  • [ ] HTTPS kullanın; session.cookie_secure = 1 ve session.cookie_httponly = 1 ayarlayın
  • [ ] Giriş sistemi ekleyin ve system/ajax.php başındaki sahte aktör satırlarını silin
  • [ ] Nginx kullanıyorsanız .htaccess çalışmaz; .sql ve .md erişimini sunucu yapılandırmasından kapatın:

```nginx
location ~* \.(sql|log|ini|bak)$ { deny all; }
```

  • [ ] Denetim tablosu için arşivleme / bölümleme planı yapın
  • [ ] audit_log yedeklerini ana veriyle aynı sıklıkta alın

Sorun Giderme

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üyorOturum düşmüş — sayfayı yenileyin. Sunucuda session.save_path yazılabilir olmalıdır.
CSV Excel'de bozuk görünüyorDosya 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 definedJavaScript yükleme sırası bozulmuş. jQuery her zaman en başta gelmelidir.
Türkçe karakterler bozukVeritabanı utf8mb4 değil. cy_audit.sql sonundaki CONVERT TO CHARACTER SET utf8mb4 satırlarını çalıştırın.
DataTables "Requested unknown parameter"`` sayısı ile sunucudan dönen dizi uzunluğu farklı. İkisini eşitleyin.
Geçmişe satır düşmüyorGüncellemede hiçbir alan değişmemiş olabilir — bu durumda kayıt bilinçli olarak yazılmaz.

Yol Haritası

  • [ ] Denetim kaydından geri alma (rollback): old_values ile kaydı eski hâline döndürme
  • [ ] full modda satır anlık görüntülerinden zaman çizelgesi görünümü
  • [ ] Kullanıcı girişi ve rol tabanlı yetkilendirme (yalnızca yöneticiler geçmişi görsün)
  • [ ] Denetim tablosu için otomatik arşivleme / bölümleme betiği
  • [ ] Webhook: belirli alanlar değiştiğinde bildirim gönderme
  • [ ] Excel (XLSX) ve PDF dışa aktarım
  • [ ] PHPUnit ile birim testleri (diff_values, redact, valid_date)
  • [ ] Koyu tema için elle açma/kapama düğmesi

Katkı

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 bildirIssues
💡 Özellik önerIssues
🔧 Kod gönderPull Requests
❓ Soru sorDiscussions

Katkı ölçütleri

  • Kod açıklamalı olsun. Bu projenin temel amacı öğretmek; yorumsuz kod PR'ı geri döner.
  • audit_log tablosuna yazma uç noktası eklemeyin. Append-only olması projenin tezidir.
  • Yeni bir yazma işlemi eklerken audit() çağrısını ve transaction'ı unutmayın.
  • Tasarım değişikliklerini style.css üzerinden yapın; cilginyazilim.css markaya aittir ve diğer projelerle ortaktır.
  • Yeni bir dış kütüphane eklemeden önce issue açıp tartışalım — proje bilinçli olarak bağımlılıksızdır.

Lisans

MIT — ticari kullanım dahil serbesttir.

Önce bir deneyin

Canlı Demoyu Aç
Diğer Örnekler

cilginyazilim.com tarafından ❤ ile geliştirildi

Faydalı bulduysanız ⭐ vermeyi unutmayın.

Kaynak Kod soldaki ağaçtan bir dosya seçin

  • assets
    • css
      • bootstrap.min.css 227.5 KB
      • cilginyazilim.css 22 KB
      • dataTables.bootstrap5.min.css 11.7 KB
      • style.css 12.6 KB
    • images
      • logo.png 70.4 KB
    • js
      • audit.js 19.9 KB
      • bootstrap.bundle.js 203.2 KB
      • dataTables.bootstrap5.min.js 2.3 KB
      • jquery-3.7.0.js 278.3 KB
      • jquery.dataTables.min.js 85.1 KB
  • docs
    • screenshots
      • 01-genel-gorunum.png 325.6 KB
      • 02-diff-detayi.png 175.4 KB
      • 03-mobil.png 206.2 KB
  • system
    • .htaccess 1.7 KB
    • ajax.php 20.4 KB
    • config.local.php.example 1.5 KB
    • config.php 10.5 KB
    • function.php 14.2 KB
  • .gitignore 1.1 KB
  • .htaccess 3.6 KB
  • CHANGELOG.md 8.3 KB
  • cy_audit.sql 8.6 KB
  • index.php 19.1 KB
  • LICENSE 1.1 KB
  • README.en.md 32.1 KB
  • README.md 42.2 KB
Dosya seçilmedi
İncelemek istediğiniz dosyayı soldaki ağaçtan seçin.

Güvenlik gereği kaynak dosyalardaki parola, API anahtarı ve benzeri gizli değerler gösterilmeden önce maskelenir (••••••••).

Sık Sorulan Sorular

"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.

Yorumlar 0 konuşma

Bu kod örneğine henüz yorum yapılmamış. Takıldığınız bir yer veya merak ettiğiniz bir ayrıntı varsa ilk soruyu siz sorun.

Soru Sor veya Yorum Yaz

Yorumunuz onaylandıktan sonra yayınlanır. Teknik sorularınıza ekibimiz yanıt verir.

Yayınlanmaz; yalnızca yanıt bildirimi için kullanılır.
En az 10 karakter.

İlgili Kod Örnekleri