Belgeler · Kurulum
Beş dakikada yayında.
GitHub’dan indir, sunucuna yükle, istersen PHP’ye bağla. Derleme adımı yok.
01Gereksinimler
| Ne | Sürüm | Not |
|---|---|---|
| Web sunucusu | Apache 2.4+ ya da Nginx | Statik dosya sunabilen her şey yeter |
| PHP | 8.1+ | Yalnızca gerçek veriye bağlayacaksan |
| MySQL / MariaDB | 8.0+ / 10.6+ | ornekler/php/schema.sql |
| HTTPS | Zorunlu | Oturum çerezleri Secure bayrağıyla gönderilir |
02İndir
git clone https://github.com/furkankirci/fkdev-admin.git
cd fkdev-adminYa da GitHub’da Code → Download ZIP. Dosyaları public_html (ya da sanal sunucunun kök klasörü) içine kopyala. .htaccess gizli dosyadır; FTP istemcinde “gizli dosyaları göster” açık olsun.
03Apache (.htaccess)
Depodaki .htaccess hazırdır: HTTPS yönlendirmesi, temiz adresler, 404 sayfası, güvenlik başlıkları, CSP, önbellek ve hassas dosyaların engellenmesi. Kendi alan adını yazman yeter.
Options -Indexes -MultiViews
DirectoryIndex index.php index.html
AddDefaultCharset UTF-8
FileETag None
ServerSignature Off
ErrorDocument 400 /404.html
ErrorDocument 403 /404.html
ErrorDocument 404 /404.html
ErrorDocument 410 /404.html
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteCond %{HTTPS} !=on
RewriteCond %{HTTP:X-Forwarded-Proto} !=https
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^ https://%1%{REQUEST_URI} [L,R=301]
RewriteCond %{THE_REQUEST} \s/+(.*/)?index\.html[\s?] [NC]
RewriteRule ^(.*/)?index\.html$ /$1 [L,R=301]
RewriteCond %{REQUEST_FILENAME} -d
RewriteCond %{REQUEST_URI} !/$
RewriteRule ^(.*)$ /$1/ [L,R=301]
RewriteRule ^404\.html$ - [L]
RewriteCond %{REQUEST_URI} !^/\.well-known/
RewriteRule (^|/)\. - [F,L]
RewriteRule ^ornekler(/|$) - [F,L]
</IfModule>
<FilesMatch "(\.(md|sql|log|ini|env|bak|old|orig|swp|sh|lock|dist)|^LICENSE|^composer\.json|^package\.json)$">
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
</FilesMatch>
<FilesMatch "^\.">
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
</FilesMatch>
<IfModule mod_mime.c>
AddType image/svg+xml .svg
AddType image/x-icon .ico
AddType application/xml .xml
AddType text/plain .txt
AddType application/json .json
AddCharset UTF-8 .html .css .js .xml .txt .svg .json
</IfModule>
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/css text/xml application/xml application/javascript text/javascript application/json image/svg+xml image/x-icon
</IfModule>
<IfModule mod_brotli.c>
AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/css text/xml application/xml application/javascript text/javascript application/json image/svg+xml
</IfModule>
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" env=HTTPS
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()"
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://ui.fkdev.sbs https://code.jquery.com https://cdn.datatables.net 'sha256-CGGEJHfqC7+LiSARylTxPdCMaADvzU9UYiv/dnwTiYU='; style-src 'self' 'unsafe-inline' https://ui.fkdev.sbs https://fonts.googleapis.com https://cdn.datatables.net; img-src 'self' data: https://ui.fkdev.sbs; font-src https://fonts.gstatic.com; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'; upgrade-insecure-requests"
Header unset X-Powered-By
<FilesMatch "\.html$">
Header set Cache-Control "no-cache, must-revalidate"
</FilesMatch>
<FilesMatch "\.php$">
Header set Cache-Control "no-store"
</FilesMatch>
<FilesMatch "\.(css|js)$">
Header set Cache-Control "public, max-age=604800, stale-while-revalidate=86400"
</FilesMatch>
<FilesMatch "\.(svg|png|ico)$">
Header set Cache-Control "public, max-age=2592000"
</FilesMatch>
<FilesMatch "^404\.html$">
Header set X-Robots-Tag "noindex, follow"
</FilesMatch>
</IfModule>
<IfModule mod_php.c>
php_flag display_errors off
php_flag expose_php off
php_value session.cookie_httponly 1
php_value session.cookie_secure 1
php_value session.use_strict_mode 1
</IfModule>04Nginx
server {
listen 443 ssl http2;
server_name admin.ornek.com;
root /var/www/fkdev-admin;
index index.html index.php;
location ~ /\.(?!well-known) { deny all; }
location ^~ /ornekler/ { deny all; }
location ~* \.(md|sql|log|ini|env|bak)$ { deny all; }
location / { try_files $uri $uri/ =404; }
error_page 404 /404.html;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location ~* \.(css|js|svg|woff2)$ { expires 7d; add_header Cache-Control "public"; }
}05CDN bağlantısı
Her sayfanın <head> bölümü fkdev UI’yi sabit sürümle ve SRI imzasıyla yükler:
<link rel="stylesheet" href="https://ui.fkdev.sbs/v2.4.1/fkdev-ui.min.css" integrity="sha384-ywbvrnvLBhspdz061sb4+HVmL4bAnbhyuwKyDq0qCBVdEeLw2Le+aT+M0XhiumnM" crossorigin="anonymous">
<link rel="stylesheet" href="https://ui.fkdev.sbs/v2.4.1/fkdev-icons.css" integrity="sha384-7yUKhYWWrNB988JPR7B5hWqmWKk50ryeZ/b688YUSNn02PjgfTkhgNDnoRUX0w4V" crossorigin="anonymous">
<script src="https://ui.fkdev.sbs/v2.4.1/fkdev-ui.min.js" integrity="sha384-M00Zh34vyulzgDN4UvQcDJteSAYjql+zh1KP3B8lMfPikmAoJLDzbKnjWiFHPsvT" crossorigin="anonymous" defer></script>Ayrıntılar için CDN belgesi.
06PHP’ye bağlama
- Veritabanını oluştur ve
ornekler/php/schema.sqldosyasını çalıştır. ornekler/php/klasörünü web kökünün dışına taşı (ör./var/www/app/);config.example.phpdosyasınıconfig.phpolarak kopyalayıp doldur.- Statik sayfayı PHP’ye çevir:
giris/index.html→giris/index.php. En üste bootstrap’ı ekle, formun içine CSRF alanını koy.
<?php require '/var/www/app/bootstrap.php'; ?>
<!-- …sayfanın geri kalanı aynı… -->
<form class="fk-form" method="post" action="/api/giris.php" data-fk-validate>
<?= csrf_field() ?>
<!-- e-posta ve şifre alanları -->
</form><?php
require '/var/www/app/giris.php';<?php
declare(strict_types=1);
$config = require __DIR__ . '/config.php';
ini_set('display_errors', '0');
ini_set('log_errors', '1');
error_reporting(E_ALL);
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('Referrer-Policy: strict-origin-when-cross-origin');
session_name($config['app']['session_name']);
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
ini_set('session.use_strict_mode', '1');
ini_set('session.use_only_cookies', '1');
session_start();
function db(): PDO
{
static $pdo = null;
global $config;
if ($pdo) {
return $pdo;
}
$c = $config['db'];
$pdo = new PDO(
sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', $c['host'], $c['name']),
$c['user'],
$c['pass'],
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
return $pdo;
}
function e(?string $s): string
{
return htmlspecialchars((string) $s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function csrf_token(): string
{
if (empty($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf'];
}
function csrf_field(): string
{
return '<input type="hidden" name="csrf" value="' . e(csrf_token()) . '">';
}
function csrf_check(): void
{
$sent = $_POST['csrf'] ?? ($_SERVER['HTTP_X_CSRF_TOKEN'] ?? '');
if (!is_string($sent) || !hash_equals(csrf_token(), $sent)) {
http_response_code(419);
exit('Oturum süresi doldu. Sayfayı yenileyip tekrar dene.');
}
}
function client_ip(): string
{
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
}
function too_many_attempts(string $email): bool
{
global $config;
$stmt = db()->prepare(
'SELECT COUNT(*) FROM login_attempts
WHERE success = 0 AND (ip = ? OR email = ?)
AND created_at > NOW() - INTERVAL ? MINUTE'
);
$stmt->execute([client_ip(), $email, (int) $config['app']['lock_minutes']]);
return (int) $stmt->fetchColumn() >= (int) $config['app']['max_attempts'];
}
function log_attempt(string $email, bool $ok): void
{
db()->prepare('INSERT INTO login_attempts (ip, email, success) VALUES (?, ?, ?)')
->execute([client_ip(), $email, $ok ? 1 : 0]);
}
function current_user(): ?array
{
if (empty($_SESSION['uid'])) {
return null;
}
$stmt = db()->prepare('SELECT id, name, email, role FROM users WHERE id = ? AND is_active = 1');
$stmt->execute([$_SESSION['uid']]);
return $stmt->fetch() ?: null;
}
function require_login(array $roles = []): array
{
$user = current_user();
if (!$user) {
header('Location: /giris/');
exit;
}
if ($roles && !in_array($user['role'], $roles, true)) {
http_response_code(403);
exit('Bu sayfayı görme yetkin yok.');
}
return $user;
}
function redirect_with(string $url, string $message, string $type = 'success'): never
{
$_SESSION['flash'] = [$message, $type];
header('Location: ' . $url);
exit;
}<?php
declare(strict_types=1);
require __DIR__ . '/bootstrap.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit;
}
csrf_check();
$email = mb_strtolower(trim((string) ($_POST['email'] ?? '')));
$password = (string) ($_POST['password'] ?? '');
if (too_many_attempts($email)) {
redirect_with('/giris/', 'Çok fazla deneme yapıldı. 15 dakika sonra tekrar dene.', 'danger');
}
$stmt = db()->prepare('SELECT id, password_hash FROM users WHERE email = ? AND is_active = 1 LIMIT 1');
$stmt->execute([$email]);
$user = $stmt->fetch();
$ok = $user && password_verify($password, $user['password_hash']);
log_attempt($email, (bool) $ok);
if (!$ok) {
usleep(random_int(200000, 400000));
redirect_with('/giris/', 'E-posta ya da şifre hatalı.', 'danger');
}
if (password_needs_rehash($user['password_hash'], PASSWORD_DEFAULT)) {
db()->prepare('UPDATE users SET password_hash = ? WHERE id = ?')
->execute([password_hash($password, PASSWORD_DEFAULT), $user['id']]);
}
session_regenerate_id(true);
$_SESSION['uid'] = (int) $user['id'];
unset($_SESSION['csrf']);
db()->prepare('UPDATE users SET last_login_at = NOW() WHERE id = ?')->execute([$user['id']]);
redirect_with('/', 'Tekrar hoş geldin!');<?php
declare(strict_types=1);
require __DIR__ . '/bootstrap.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit;
}
csrf_check();
$name = trim((string) ($_POST['name'] ?? ''));
$email = mb_strtolower(trim((string) ($_POST['email'] ?? '')));
$password = (string) ($_POST['password'] ?? '');
$confirm = (string) ($_POST['password_confirm'] ?? '');
$terms = isset($_POST['terms']);
$errors = [];
if (mb_strlen($name) < 2 || mb_strlen($name) > 120) $errors[] = 'Ad 2–120 karakter olmalı.';
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errors[] = 'Geçerli bir e-posta yaz.';
if (mb_strlen($password) < 10) $errors[] = 'Şifre en az 10 karakter olmalı.';
if (!hash_equals($password, $confirm)) $errors[] = 'Şifreler aynı değil.';
if (!$terms) $errors[] = 'Kullanım koşullarını onaylaman gerekiyor.';
if ($errors) {
redirect_with('/kayit/', implode(' ', $errors), 'danger');
}
try {
db()->prepare('INSERT INTO users (name, email, password_hash) VALUES (?, ?, ?)')
->execute([$name, $email, password_hash($password, PASSWORD_DEFAULT)]);
} catch (PDOException $ex) {
if ($ex->getCode() === '23000') {
redirect_with('/kayit/', 'Bu e-posta ile hesap açılamadı. Hesabın varsa giriş yap.', 'danger');
}
error_log($ex->getMessage());
redirect_with('/kayit/', 'Şu an kayıt yapılamıyor, biraz sonra tekrar dene.', 'danger');
}
session_regenerate_id(true);
$_SESSION['uid'] = (int) db()->lastInsertId();
redirect_with('/', 'Hesabın oluşturuldu, hoş geldin!');<?php
declare(strict_types=1);
require __DIR__ . '/bootstrap.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit;
}
csrf_check();
$_SESSION = [];
$p = session_get_cookie_params();
setcookie(session_name(), '', [
'expires' => time() - 3600,
'path' => $p['path'],
'secure' => $p['secure'],
'httponly' => $p['httponly'],
'samesite' => $p['samesite'],
]);
session_destroy();
header('Location: /giris/');
exit;07Panel kartlarını veriye bağlama
<?php
$user = require_login(['admin', 'editor']);
$ciro = db()->query("SELECT COALESCE(SUM(total), 0) FROM orders WHERE created_at >= DATE_FORMAT(NOW(), '%Y-%m-01') AND status <> 'İade'")->fetchColumn();
$bekleyen = db()->query("SELECT COUNT(*) FROM orders WHERE status = 'Bekliyor'")->fetchColumn();
?>
<article class="ad-kpi">
<div class="ad-kpi-top"><span class="ad-kpi-label"><span class="ad-ico"><i class="fk-i fk-i-lira" aria-hidden="true"></i></span>Ciro</span></div>
<strong>₺<?= number_format((float) $ciro, 0, ',', '.') ?></strong>
<small>Bu ay</small>
</article>
<p><?= e($user['name']) ?>, <?= (int) $bekleyen ?> sipariş onay bekliyor.</p><?php if (!empty($_SESSION['flash'])): [$m, $t] = $_SESSION['flash']; unset($_SESSION['flash']); ?>
<script>addEventListener('DOMContentLoaded', () => FK.toast(<?= json_encode($m) ?>, <?= json_encode($t) ?>));</script>
<?php endif; ?>CSP ve satır içi betikDepodaki CSP satır içi betikleri engeller (head’deki tema betiği hariç, o hash ile izinli). Bildirim için bu küçük betiği kullanacaksan ya ayrı bir .js dosyasına taşı ya da CSP’ye nonce ekle.