Tiếp Phần 7 (website đã bán hàng được cho khách). Hôm nay làm khu vực QUẢN TRỊ (admin) để chủ shop tự quản lý danh mục, sản phẩm, đơn hàng qua giao diện web - không cần vào phpMyAdmin sửa tay nữa. Đây là phần dài nhất loạt bài, các bạn tạo đủ các file trong thư mục admin/ như cấu trúc ở Phần 1 rồi dán lần lượt nhé. Trước tiên, thêm đoạn CSS sau vào CUỐI file assets/css/style.css (đã tạo ở Phần 3) để có giao diện cho khu quản trị: .admin-wrap { display:flex; min-height:100vh; } .admin-sidebar { width:220px; background:#1f2937; color:#fff; flex-shrink:0; } .admin-logo { padding:20px; font-size:20px; font-weight:bold; border-bottom:1px solid #374151; } .admin-logo span { color:#e53935; } .admin-sidebar nav { display:flex; flex-direction:column; padding:10px 0; } .admin-sidebar nav a { padding:12px 20px; color:#d1d5db; } .admin-sidebar nav a:hover { background:#374151; color:#fff; } .admin-content { flex:1; padding:24px; } .admin-topbar { text-align:right; margin-bottom:16px; color:#555; font-size:14px; } .dashboard-cards { display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:16px; } .dash-card { background:#fff; padding:20px; border-radius:8px; box-shadow:0 1px 4px rgba(0,0,0,.08); display:flex; flex-direction:column; gap:6px; } .dash-number { font-size:24px; font-weight:bold; color:#e53935; } .product-thumb-small { width:50px; height:50px; object-fit:cover; border-radius:4px; } .status-badge { padding:4px 10px; border-radius:12px; font-size:12px; } .status-pending { background:#fff3cd; color:#856404; } .status-confirmed { background:#cce5ff; color:#004085; } .status-shipping { background:#d1ecf1; color:#0c5460; } .status-completed { background:#d4edda; color:#155724; } .status-cancelled { background:#f8d7da; color:#721c24; } ============================== 1. FILE admin/includes/admin_header.php ============================== <?php ?> <!DOCTYPE html> <html lang="vi"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title><?= isset($page_title) ? e($page_title) . ' - Quan tri' : 'Quan tri ShopVN' ?></title> <link rel="stylesheet" href="../assets/css/style.css"> </head> <body> <div class="admin-wrap"> <aside class="admin-sidebar"> <div class="admin-logo">Shop<span>VN</span> Admin</div> <nav> <a href="index.php">Dashboard</a> <a href="categories.php">Danh muc</a> <a href="products.php">San pham</a> <a href="orders.php">Don hang</a> <a href="logout.php">Dang xuat</a> </nav> </aside> <main class="admin-content"> <div class="admin-topbar">Xin chao, <?= e($_SESSION['user_name'] ?? '') ?></div> ============================== 2. FILE admin/includes/admin_footer.php ============================== </main> </div> </body> </html> ============================== 3. FILE admin/login.php (đăng nhập quản trị - tách riêng khỏi login.php của khách) ============================== <?php require_once __DIR__ . '/../includes/functions.php'; if (is_admin()) { redirect('index.php'); } $error = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { csrf_verify(); $email = trim($_POST['email'] ?? ''); $password = $_POST['password'] ?? ''; // Chi cho phep dang nhap neu dung email/mat khau VA role la admin // - mot khach hang thuong khong the vao duoc khu vuc nay du biet dung mat khau cua ho $stmt = $pdo->prepare('SELECT * FROM users WHERE email = ? AND role = "admin" LIMIT 1'); $stmt->execute([$email]); $user = $stmt->fetch(); if ($user && password_verify($password, $user['password_hash'])) { $_SESSION['user_id'] = (int) $user['id']; $_SESSION['user_name'] = $user['full_name']; $_SESSION['user_role'] = 'admin'; session_regenerate_id(true); redirect('index.php'); } $error = 'Email hoac mat khau khong dung, hoac tai khoan khong co quyen quan tri.'; } ?> <!DOCTYPE html> <html lang="vi"> <head> <meta charset="UTF-8"> <title>Dang nhap quan tri - ShopVN</title> <link rel="stylesheet" href="../assets/css/style.css"> </head> <body> <main class="container" style="padding:60px 16px;"> <div class="form-box"> <h1 class="page-title">Dang nhap quan tri</h1> <?php if ($error): ?> <div class="alert alert-error"><?= e($error) ?></div> <?php endif; ?> <form action="login.php" method="post"> <?= csrf_field() ?> <div class="form-group"> <label>Email</label> <input type="email" name="email" required> </div> <div class="form-group"> <label>Mat khau</label> <input type="password" name="password" required> </div> <button type="submit" class="btn" style="width:100%;">Dang nhap</button> </form> </div> </main> </body> </html> ============================== 4. FILE admin/logout.php ============================== <?php require_once __DIR__ . '/../includes/functions.php'; $_SESSION = []; session_destroy(); redirect('login.php'); ============================== 5. FILE admin/index.php (dashboard) ============================== <?php require_once __DIR__ . '/../includes/functions.php'; require_admin(); $page_title = 'Dashboard'; $total_products = (int) $pdo->query('SELECT COUNT(*) FROM products')->fetchColumn(); $total_orders = (int) $pdo->query('SELECT COUNT(*) FROM orders')->fetchColumn(); $pending_orders = (int) $pdo->query("SELECT COUNT(*) FROM orders WHERE status = 'pending'")->fetchColumn(); $total_revenue = (float) $pdo->query("SELECT COALESCE(SUM(total_amount),0) FROM orders WHERE status != 'cancelled'")->fetchColumn(); $recent_orders = $pdo->query('SELECT * FROM orders ORDER BY created_at DESC LIMIT 5')->fetchAll(); require __DIR__ . '/includes/admin_header.php'; ?> <h1 class="page-title">Tong quan</h1> <div class="dashboard-cards"> <div class="dash-card"><span class="dash-number"><?= $total_products ?></span><span>San pham</span></div> <div class="dash-card"><span class="dash-number"><?= $total_orders ?></span><span>Don hang</span></div> <div class="dash-card"><span class="dash-number"><?= $pending_orders ?></span><span>Cho xu ly</span></div> <div class="dash-card"><span class="dash-number"><?= format_price($total_revenue) ?></span><span>Doanh thu</span></div> </div> <h2 style="margin:24px 0 10px; font-size:16px;">Don hang gan day</h2> <table class="data-table"> <thead><tr><th>Ma</th><th>Khach hang</th><th>Tong tien</th><th>Trang thai</th><th></th></tr></thead> <tbody> <?php foreach ($recent_orders as $o): ?> <tr> <td>#<?= $o['id'] ?></td> <td><?= e($o['full_name']) ?></td> <td><?= format_price($o['total_amount']) ?></td> <td><span class="status-badge status-<?= e($o['status']) ?>"><?= e($o['status']) ?></span></td> <td><a href="order_detail.php?id=<?= $o['id'] ?>">Xem</a></td> </tr> <?php endforeach; ?> </tbody> </table> <?php require __DIR__ . '/includes/admin_footer.php'; ?> ============================== 6. FILE admin/categories.php ============================== <?php require_once __DIR__ . '/../includes/functions.php'; require_admin(); $page_title = 'Danh muc'; if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delete') { csrf_verify(); $id = (int) $_POST['id']; $stmt = $pdo->prepare('DELETE FROM categories WHERE id = ?'); $stmt->execute([$id]); redirect('categories.php'); } $categories = $pdo->query( 'SELECT c.*, COUNT(p.id) AS product_count FROM categories c LEFT JOIN products p ON p.category_id = c.id GROUP BY c.id ORDER BY c.name' )->fetchAll(); require __DIR__ . '/includes/admin_header.php'; ?> <div style="display:flex; justify-content:space-between; align-items:center;"> <h1 class="page-title">Danh muc san pham</h1> <a href="category_form.php" class="btn">+ Them danh muc</a> </div> <table class="data-table" style="margin-top:14px;"> <thead><tr><th>Ten</th><th>Slug</th><th>So san pham</th><th></th></tr></thead> <tbody> <?php foreach ($categories as $c): ?> <tr> <td><?= e($c['name']) ?></td> <td><?= e($c['slug']) ?></td> <td><?= $c['product_count'] ?></td> <td> <a href="category_form.php?id=<?= $c['id'] ?>">Sua</a> <form action="categories.php" method="post" style="display:inline;" onsubmit="return confirm('Xoa danh muc nay? San pham thuoc danh muc cung se bi xoa.');"> <?= csrf_field() ?> <input type="hidden" name="action" value="delete"> <input type="hidden" name="id" value="<?= $c['id'] ?>"> <button type="submit" style="border:none;background:none;color:#e53935;cursor:pointer;">Xoa</button> </form> </td> </tr> <?php endforeach; ?> </tbody> </table> <?php require __DIR__ . '/includes/admin_footer.php'; ?> ============================== 7. FILE admin/category_form.php ============================== <?php require_once __DIR__ . '/../includes/functions.php'; require_admin(); $id = (int) ($_GET['id'] ?? 0); $category = ['name' => '']; if ($id) { $stmt = $pdo->prepare('SELECT * FROM categories WHERE id = ?'); $stmt->execute([$id]); $category = $stmt->fetch() ?: $category; } $page_title = $id ? 'Sua danh muc' : 'Them danh muc'; $errors = []; if ($_SERVER['REQUEST_METHOD'] === 'POST') { csrf_verify(); $name = trim($_POST['name'] ?? ''); if ($name === '') { $errors[] = 'Vui long nhap ten danh muc.'; } if (empty($errors)) { $slug = slugify($name); if ($id) { $stmt = $pdo->prepare('UPDATE categories SET name = ?, slug = ? WHERE id = ?'); $stmt->execute([$name, $slug, $id]); } else { $stmt = $pdo->prepare('INSERT INTO categories (name, slug) VALUES (?, ?)'); $stmt->execute([$name, $slug]); } redirect('categories.php'); } $category['name'] = $name; } require __DIR__ . '/includes/admin_header.php'; ?> <h1 class="page-title"><?= e($page_title) ?></h1> <?php foreach ($errors as $err): ?> <div class="alert alert-error"><?= e($err) ?></div> <?php endforeach; ?> <form action="category_form.php<?= $id ? '?id=' . $id : '' ?>" method="post" class="form-box"> <?= csrf_field() ?> <div class="form-group"> <label>Ten danh muc</label> <input type="text" name="name" value="<?= e($category['name']) ?>" required> </div> <button type="submit" class="btn">Luu</button> </form> <?php require __DIR__ . '/includes/admin_footer.php'; ?> ============================== 8. FILE admin/products.php ============================== <?php require_once __DIR__ . '/../includes/functions.php'; require_admin(); $page_title = 'San pham'; if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delete') { csrf_verify(); $id = (int) $_POST['id']; $stmt = $pdo->prepare('DELETE FROM products WHERE id = ?'); $stmt->execute([$id]); redirect('products.php'); } $products = $pdo->query( 'SELECT p.*, c.name AS category_name FROM products p JOIN categories c ON c.id = p.category_id ORDER BY p.created_at DESC' )->fetchAll(); require __DIR__ . '/includes/admin_header.php'; ?> <div style="display:flex; justify-content:space-between; align-items:center;"> <h1 class="page-title">San pham</h1> <a href="product_form.php" class="btn">+ Them san pham</a> </div> <table class="data-table" style="margin-top:14px;"> <thead><tr><th>Anh</th><th>Ten</th><th>Danh muc</th><th>Gia</th><th>Ton kho</th><th>Trang thai</th><th></th></tr></thead> <tbody> <?php foreach ($products as $p): ?> <tr> <td><img src="../assets/uploads/products/<?= e($p['image'] ?: 'no-image.png') ?>" class="product-thumb-small"></td> <td><?= e($p['name']) ?></td> <td><?= e($p['category_name']) ?></td> <td><?= format_price($p['sale_price'] ?: $p['price']) ?></td> <td><?= (int) $p['stock'] ?></td> <td><?= $p['is_active'] ? 'Dang ban' : 'An' ?></td> <td> <a href="product_form.php?id=<?= $p['id'] ?>">Sua</a> <form action="products.php" method="post" style="display:inline;" onsubmit="return confirm('Xoa san pham nay?');"> <?= csrf_field() ?> <input type="hidden" name="action" value="delete"> <input type="hidden" name="id" value="<?= $p['id'] ?>"> <button type="submit" style="border:none;background:none;color:#e53935;cursor:pointer;">Xoa</button> </form> </td> </tr> <?php endforeach; ?> </tbody> </table> <?php require __DIR__ . '/includes/admin_footer.php'; ?> ============================== 9. FILE admin/product_form.php (thêm/sửa sản phẩm + upload ảnh) ============================== <?php require_once __DIR__ . '/../includes/functions.php'; require_admin(); $id = (int) ($_GET['id'] ?? 0); $product = [ 'category_id' => '', 'name' => '', 'description' => '', 'price' => '', 'sale_price' => '', 'stock' => 0, 'image' => '', 'is_active' => 1, ]; if ($id) { $stmt = $pdo->prepare('SELECT * FROM products WHERE id = ?'); $stmt->execute([$id]); $product = $stmt->fetch() ?: $product; } $page_title = $id ? 'Sua san pham' : 'Them san pham'; $categories = $pdo->query('SELECT id, name FROM categories ORDER BY name')->fetchAll(); $errors = []; // Chi cho phep dinh dang anh thong dung, gioi han 2MB $allowed_ext = ['jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'webp' => 'image/webp']; $max_size = 2 * 1024 * 1024; if ($_SERVER['REQUEST_METHOD'] === 'POST') { csrf_verify(); $category_id = (int) ($_POST['category_id'] ?? 0); $name = trim($_POST['name'] ?? ''); $description = trim($_POST['description'] ?? ''); $price = (int) ($_POST['price'] ?? 0); $sale_price = $_POST['sale_price'] !== '' ? (int) $_POST['sale_price'] : null; $stock = (int) ($_POST['stock'] ?? 0); $is_active = isset($_POST['is_active']) ? 1 : 0; $image_name = $product['image']; if ($category_id <= 0) $errors[] = 'Vui long chon danh muc.'; if ($name === '') $errors[] = 'Vui long nhap ten san pham.'; if ($price <= 0) $errors[] = 'Gia ban phai lon hon 0.'; if ($sale_price !== null && $sale_price >= $price) $errors[] = 'Gia khuyen mai phai nho hon gia goc.'; if (!empty($_FILES['image']['name']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) { $ext = strtolower(pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION)); $tmp_path = $_FILES['image']['tmp_name']; if (!isset($allowed_ext[$ext])) { $errors[] = 'Chi cho phep anh dinh dang JPG, PNG hoac WEBP.'; } elseif ($_FILES['image']['size'] > $max_size) { $errors[] = 'Anh khong duoc vuot qua 2MB.'; } else { // Kiem tra MIME type THAT cua file bang finfo, khong tin duoi file $finfo = finfo_open(FILEINFO_MIME_TYPE); $real_mime = finfo_file($finfo, $tmp_path); finfo_close($finfo); if ($real_mime !== $allowed_ext[$ext]) { $errors[] = 'File anh khong hop le (noi dung file khong khop voi duoi file).'; } else { $new_name = bin2hex(random_bytes(16)) . '.' . $ext; $dest = __DIR__ . '/../assets/uploads/products/' . $new_name; if (move_uploaded_file($tmp_path, $dest)) { $image_name = $new_name; } else { $errors[] = 'Khong the luu anh, vui long thu lai.'; } } } } if (empty($errors)) { $slug = slugify($name); if ($id) { $stmt = $pdo->prepare( 'UPDATE products SET category_id=?, name=?, slug=?, description=?, price=?, sale_price=?, image=?, stock=?, is_active=? WHERE id=?' ); $stmt->execute([$category_id, $name, $slug, $description, $price, $sale_price, $image_name, $stock, $is_active, $id]); } else { $stmt = $pdo->prepare( 'INSERT INTO products (category_id, name, slug, description, price, sale_price, image, stock, is_active) VALUES (?,?,?,?,?,?,?,?,?)' ); $stmt->execute([$category_id, $name, $slug, $description, $price, $sale_price, $image_name, $stock, $is_active]); } redirect('products.php'); } $product = ['category_id' => $category_id, 'name' => $name, 'description' => $description, 'price' => $price, 'sale_price' => $sale_price, 'stock' => $stock, 'is_active' => $is_active, 'image' => $image_name]; } require __DIR__ . '/includes/admin_header.php'; ?> <h1 class="page-title"><?= e($page_title) ?></h1> <?php foreach ($errors as $err): ?> <div class="alert alert-error"><?= e($err) ?></div> <?php endforeach; ?> <form action="product_form.php<?= $id ? '?id=' . $id : '' ?>" method="post" enctype="multipart/form-data" class="form-box" style="max-width:600px;"> <?= csrf_field() ?> <div class="form-group"> <label>Danh muc</label> <select name="category_id" required> <option value="">-- Chon danh muc --</option> <?php foreach ($categories as $c): ?> <option value="<?= $c['id'] ?>" <?= (int) $product['category_id'] === (int) $c['id'] ? 'selected' : '' ?>><?= e($c['name']) ?></option> <?php endforeach; ?> </select> </div> <div class="form-group"> <label>Ten san pham</label> <input type="text" name="name" value="<?= e($product['name']) ?>" required> </div> <div class="form-group"> <label>Mo ta</label> <textarea name="description" rows="4"><?= e($product['description']) ?></textarea> </div> <div class="form-group"> <label>Gia goc (d)</label> <input type="number" name="price" value="<?= e((string) $product['price']) ?>" required> </div> <div class="form-group"> <label>Gia khuyen mai (d) - de trong neu khong giam gia</label> <input type="number" name="sale_price" value="<?= e((string) ($product['sale_price'] ?? '')) ?>"> </div> <div class="form-group"> <label>Ton kho</label> <input type="number" name="stock" value="<?= e((string) $product['stock']) ?>" required> </div> <div class="form-group"> <label>Hinh anh (JPG/PNG/WEBP, toi da 2MB)</label> <?php if (!empty($product['image'])): ?> <p><img src="../assets/uploads/products/<?= e($product['image']) ?>" style="width:80px;border-radius:4px;"></p> <?php endif; ?> <input type="file" name="image" accept=".jpg,.jpeg,.png,.webp"> </div> <div class="form-group"> <label style="font-weight:normal;"><input type="checkbox" name="is_active" <?= $product['is_active'] ? 'checked' : '' ?>> Hien thi san pham nay tren website</label> </div> <button type="submit" class="btn">Luu san pham</button> </form> <?php require __DIR__ . '/includes/admin_footer.php'; ?> ============================== 10. FILE admin/orders.php ============================== <?php require_once __DIR__ . '/../includes/functions.php'; require_admin(); $page_title = 'Don hang'; $status_filter = $_GET['status'] ?? ''; $sql = 'SELECT * FROM orders'; $params = []; if ($status_filter !== '') { $sql .= ' WHERE status = ?'; $params[] = $status_filter; } $sql .= ' ORDER BY created_at DESC'; $stmt = $pdo->prepare($sql); $stmt->execute($params); $orders = $stmt->fetchAll(); $status_labels = [ 'pending' => 'Cho xac nhan', 'confirmed' => 'Da xac nhan', 'shipping' => 'Dang giao', 'completed' => 'Hoan thanh', 'cancelled' => 'Da huy', ]; require __DIR__ . '/includes/admin_header.php'; ?> <h1 class="page-title">Don hang</h1> <div class="category-bar"> <a href="orders.php" class="<?= $status_filter === '' ? 'active' : '' ?>">Tat ca</a> <?php foreach ($status_labels as $key => $label): ?> <a href="orders.php?status=<?= $key ?>" class="<?= $status_filter === $key ? 'active' : '' ?>"><?= e($label) ?></a> <?php endforeach; ?> </div> <table class="data-table"> <thead><tr><th>Ma</th><th>Khach hang</th><th>SDT</th><th>Tong tien</th><th>Trang thai</th><th>Ngay dat</th><th></th></tr></thead> <tbody> <?php foreach ($orders as $o): ?> <tr> <td>#<?= $o['id'] ?></td> <td><?= e($o['full_name']) ?></td> <td><?= e($o['phone']) ?></td> <td><?= format_price($o['total_amount']) ?></td> <td><span class="status-badge status-<?= e($o['status']) ?>"><?= e($status_labels[$o['status']] ?? $o['status']) ?></span></td> <td><?= date('d/m/Y H:i', strtotime($o['created_at'])) ?></td> <td><a href="order_detail.php?id=<?= $o['id'] ?>">Chi tiet</a></td> </tr> <?php endforeach; ?> <?php if (empty($orders)): ?> <tr><td colspan="7">Khong co don hang nao.</td></tr> <?php endif; ?> </tbody> </table> <?php require __DIR__ . '/includes/admin_footer.php'; ?> ============================== 11. FILE admin/order_detail.php (xem chi tiết + đổi trạng thái) ============================== <?php require_once __DIR__ . '/../includes/functions.php'; require_admin(); $id = (int) ($_GET['id'] ?? 0); $stmt = $pdo->prepare('SELECT * FROM orders WHERE id = ?'); $stmt->execute([$id]); $order = $stmt->fetch(); if (!$order) { http_response_code(404); die('Khong tim thay don hang.'); } $page_title = 'Don hang #' . $order['id']; $valid_statuses = ['pending', 'confirmed', 'shipping', 'completed', 'cancelled']; if ($_SERVER['REQUEST_METHOD'] === 'POST') { csrf_verify(); $new_status = $_POST['status'] ?? ''; if (in_array($new_status, $valid_statuses, true)) { $stmt = $pdo->prepare('UPDATE orders SET status = ? WHERE id = ?'); $stmt->execute([$new_status, $id]); redirect('order_detail.php?id=' . $id); } } $stmt = $pdo->prepare('SELECT * FROM order_items WHERE order_id = ?'); $stmt->execute([$id]); $items = $stmt->fetchAll(); require __DIR__ . '/includes/admin_header.php'; ?> <h1 class="page-title">Don hang #<?= $order['id'] ?></h1> <div style="display:flex; gap:20px; flex-wrap:wrap;"> <div class="form-box" style="flex:1; min-width:280px;"> <h2 style="font-size:15px; margin-bottom:10px;">Thong tin giao hang</h2> <p>Nguoi nhan: <?= e($order['full_name']) ?></p> <p>SDT: <?= e($order['phone']) ?></p> <p>Dia chi: <?= e($order['address']) ?></p> <p>Ghi chu: <?= e($order['note'] ?: '(khong co)') ?></p> <p>Thanh toan: <?= $order['payment_method'] === 'cod' ? 'COD' : 'Chuyen khoan' ?></p> <form action="order_detail.php?id=<?= $id ?>" method="post" style="margin-top:14px;"> <?= csrf_field() ?> <div class="form-group"> <label>Trang thai don hang</label> <select name="status"> <?php foreach ($valid_statuses as $s): ?> <option value="<?= $s ?>" <?= $order['status'] === $s ? 'selected' : '' ?>><?= e($s) ?></option> <?php endforeach; ?> </select> </div> <button type="submit" class="btn">Cap nhat trang thai</button> </form> </div> <div class="form-box" style="flex:1; min-width:280px;"> <h2 style="font-size:15px; margin-bottom:10px;">San pham</h2> <?php foreach ($items as $it): ?> <div style="display:flex; justify-content:space-between; padding:6px 0; border-bottom:1px solid #eee; font-size:14px;"> <span><?= e($it['product_name']) ?> x<?= $it['quantity'] ?></span> <span><?= format_price($it['subtotal']) ?></span> </div> <?php endforeach; ?> <div style="display:flex; justify-content:space-between; padding-top:12px;"> <strong>Tong cong</strong> <strong class="price"><?= format_price($order['total_amount']) ?></strong> </div> </div> </div> <?php require __DIR__ . '/includes/admin_footer.php'; ?> ============================== GIẢI THÍCH QUAN TRỌNG ============================== - Tất cả file trong admin/ đều gọi require_admin() ngay dòng đầu (sau khi include functions.php) - đây là "chốt chặn" bắt buộc, chặn cả người dùng chưa đăng nhập LẪN khách hàng thường (role customer) cố tình gõ thẳng URL vào khu quản trị. - admin/login.php dùng câu SQL "WHERE email = ? AND role = 'admin'" - lọc ngay từ CSDL, không load tài khoản customer lên rồi mới check role bằng PHP, giảm thiểu rủi ro nếu code check sau đó có lỗi logic. - Xoá danh mục/sản phẩm dùng FORM POST (không dùng link <a href="...delete...">) kèm CSRF token: link GET để xoá dữ liệu là thói quen xấu (dễ bị trình duyệt prefetch hoặc bot quét link rồi xoá nhầm dữ liệu, lại còn dễ bị giả mạo CSRF hơn). - Upload ảnh sản phẩm kiểm tra ĐỦ 3 lớp: whitelist đuôi file, giới hạn dung lượng, và kiểm tra MIME type THẬT bằng finfo_file() (không tin đuôi file người dùng đặt tên) - vì kẻ xấu hoàn toàn có thể đổi tên file "shell.php" thành "shell.jpg" để qua mặt việc chỉ kiểm tra đuôi file. - Tên file ảnh sau khi upload được đổi thành chuỗi ngẫu nhiên (bin2hex(random_bytes(16))) - tránh bị ghi đè file trùng tên và tránh lộ thông tin qua tên file gốc. Phần 9 tiếp theo mình sẽ tổng hợp lại và bổ sung thêm các lớp bảo mật quan trọng khác cho toàn bộ dự án (chặn truy cập trực tiếp thư mục nhạy cảm, bảo vệ thư mục uploads khỏi bị thực thi mã độc, cấu hình PHP an toàn cho production...). Hẹn gặp lại! --- Hết Phần 8 --- ---------------------------------------- MỤC LỤC LOẠT BÀI: 1. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 1: Tổng Quan Dự Án, Yêu Cầu Môi Trường & Cấu Trúc Thư Mục - https://diendan.anyvlogs.info/chu-de/457-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-1-tong-quan-du-an-yeu-cau-moi-truong-cau-truc-thu-muc 2. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 2: Thiết Kế Cơ Sở Dữ Liệu (Database) Đầy Đủ Cho Website Bán Hàng - https://diendan.anyvlogs.info/chu-de/458-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-2-thiet-ke-co-so-du-lieu-database-day-du-cho-website-ban-hang 3. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 3: Dựng Khung Ứng Dụng - Kết Nối PDO, Hàm Dùng Chung, Giao Diện Chung - https://diendan.anyvlogs.info/chu-de/459-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-3-dung-khung-ung-dung-ket-noi-pdo-ham-dung-chung-giao-dien-chung 4. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 4: Trang Chủ, Danh Mục Sản Phẩm & Chi Tiết Sản Phẩm - https://diendan.anyvlogs.info/chu-de/460-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-4-trang-chu-danh-muc-san-pham-chi-tiet-san-pham 5. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 5: Xây Dựng Giỏ Hàng (Cart) Bằng Session - https://diendan.anyvlogs.info/chu-de/461-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-5-xay-dung-gio-hang-cart-bang-session 6. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 6: Đăng Ký, Đăng Nhập, Đăng Xuất Thành Viên - https://diendan.anyvlogs.info/chu-de/462-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-6-dang-ky-dang-nhap-dang-xuat-thanh-vien 7. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 7: Đặt Hàng, Thanh Toán COD & Lịch Sử Đơn Hàng - https://diendan.anyvlogs.info/chu-de/463-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-7-dat-hang-thanh-toan-cod-lich-su-don-hang >> 8. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 8: Trang Quản Trị (Admin) - Quản Lý Danh Mục, Sản Phẩm, Đơn Hàng - https://diendan.anyvlogs.info/chu-de/464-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-8-trang-quan-tri-admin-quan-ly-danh-muc-san-pham-don-hang 9. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 9: Bảo Mật Website (CSRF, XSS, SQL Injection, Upload An Toàn) - https://diendan.anyvlogs.info/chu-de/465-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-9-bao-mat-website-csrf-xss-sql-injection-upload-an-toan 10. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 10 (Kết Thúc): Tối Ưu Hiệu Năng & Triển Khai Lên Hosting Thật - https://diendan.anyvlogs.info/chu-de/466-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-10-ket-thuc-toi-uu-hieu-nang-trien-khai-len-hosting-that Phần trước: https://diendan.anyvlogs.info/chu-de/463-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-7-dat-hang-thanh-toan-cod-lich-su-don-hang Phần sau: https://diendan.anyvlogs.info/chu-de/465-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-9-bao-mat-website-csrf-xss-sql-injection-upload-an-toan