-
Notifications
You must be signed in to change notification settings - Fork 0
Build GENZMOVIE full-stack PHP movie streaming platform scaffold #44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,28 +1,69 @@ | ||
| # Decrypt | ||
| ### [+] Created By HTR-TECH (@***tahmid.rayat***) | ||
| ### [+] Disclaimer : | ||
| ***Decrypter is a tool to decrypt Encrypted Bash Scripts into a Readable Format.This Tool is created for Educational Purpose only.I am not responsible for any misuse of this tool.*** | ||
| # GENZMOVIE | ||
|
|
||
| <img src="https://raw.githubusercontent.com/htr-tech/release-download/master/images/decrypter.png" alt="" border="0" /> | ||
| GENZMOVIE is a PHP + MySQL movie streaming website with OPhim auto crawler, admin dashboard, ad monetization, user accounts, watchlist/history, SEO routes, and responsive UI. | ||
|
|
||
| ### [+] Installation | ||
| ```apt update``` | ||
| ## Project Structure | ||
|
|
||
| ```apt install git python2 -y``` | ||
| ```text | ||
| genzmovie/ | ||
| ├── config/ | ||
| ├── controllers/ | ||
| ├── models/ | ||
| ├── views/ | ||
| ├── assets/ | ||
| ├── uploads/ | ||
| ├── admin/ | ||
| ├── crawler/ | ||
| ├── api/ | ||
| ├── includes/ | ||
| ├── database/ | ||
| ├── index.php | ||
| └── .htaccess | ||
| ``` | ||
|
|
||
| ```git clone https://github.com/hax0rtahm1d/decrypt``` | ||
| ## Features | ||
|
|
||
| ```cd decrypt``` | ||
| - Netflix-style homepage with featured slider and category blocks. | ||
| - Search + autocomplete + filter by genre/year/country/quality. | ||
| - Movie details with metadata, episodes, and related movies. | ||
| - Player supports iframe embed and direct MP4/HLS source. | ||
| - Multiple server episode links. | ||
| - User register/login/logout with dashboard (favorites + watch history). | ||
| - Ad monetization: header/sidebar/popup/footer/video pre-roll placements. | ||
| - Admin dashboard for crawler trigger, ad management, overview of movies/users/comments. | ||
| - OPhim crawler imports movie metadata + episodes + links. | ||
| - SEO: friendly route `/phim/{slug}` and XML sitemap endpoint. | ||
| - Security basics: password hashing, prepared statements, CSRF token, output escaping. | ||
|
|
||
| ```python2 dec.py``` | ||
| ## Installation Guide | ||
|
|
||
| ### Or, Use Single Command | ||
| 1. **Copy files** into web root (Apache/Nginx + PHP 8+). | ||
| 2. **Create database**: | ||
| ```bash | ||
| mysql -u root -p < genzmovie/database/schema.sql | ||
| ``` | ||
| 3. **Update DB credentials** in `genzmovie/config/config.php`. | ||
| 4. **Set document root** to project root or `/genzmovie` and enable mod_rewrite. | ||
| 5. **Open website**: | ||
| - Frontend: `http://localhost/genzmovie/` | ||
| - Admin: `http://localhost/genzmovie/admin/login.php` | ||
| 6. **Default admin account**: | ||
| - Email: `admin@genzmovie.local` | ||
| - Password: `admin123` | ||
|
|
||
| ``` | ||
| apt update && apt install git python2 -y && git clone https://github.com/hax0rtahm1d/decrypt && cd decrypt && python2 dec.py | ||
| ``` | ||
| ## Crawler | ||
|
|
||
| - Manual import: use **Import từ OPhim** button in `/admin`. | ||
| - Cronjob: | ||
| ```bash | ||
| */30 * * * * php /path/to/genzmovie/crawler/ophim_crawler.php | ||
| ``` | ||
|
|
||
| ## Sitemap | ||
|
|
||
| - XML sitemap endpoint: `http://localhost/genzmovie/api/sitemap.php` | ||
|
|
||
| ## Notes | ||
|
|
||
| ## [+] Find Me on : | ||
| [](https://github.com/htr-tech) | ||
| [](https://www.instagram.com/tahmid.rayat) | ||
| [](https://m.me/tahmid.rayat.official) | ||
| - Add your Google AdSense / ad network code at admin ads form. | ||
| - For production, configure HTTPS, strict CSP, and hardened session settings. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| RewriteEngine On | ||
| RewriteCond %{REQUEST_FILENAME} !-f | ||
| RewriteCond %{REQUEST_FILENAME} !-d | ||
| RewriteRule ^phim/([a-zA-Z0-9-]+)/?$ index.php?route=movie&slug=$1 [QSA,L] | ||
| RewriteRule ^$ index.php?route=home [QSA,L] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| <?php | ||
| require_once __DIR__ . '/../config/config.php'; | ||
| require_once __DIR__ . '/../config/database.php'; | ||
| require_once __DIR__ . '/../includes/helpers.php'; | ||
| require_once __DIR__ . '/../includes/auth.php'; | ||
| require_once __DIR__ . '/../includes/csrf.php'; | ||
| require_admin(); | ||
|
|
||
| $pdo = Database::connection(); | ||
| $message = null; | ||
|
|
||
| if ($_SERVER['REQUEST_METHOD'] === 'POST' && validate_csrf($_POST['csrf_token'] ?? null)) { | ||
| if (($_POST['action'] ?? '') === 'import_ophim') { | ||
| include __DIR__ . '/../crawler/ophim_crawler.php'; | ||
| $message = 'Đã chạy crawler OPhim.'; | ||
| } | ||
| if (($_POST['action'] ?? '') === 'add_ad') { | ||
| $stmt = $pdo->prepare('INSERT INTO ads (name, position, ad_type, ad_code, status, created_at, updated_at) VALUES (:name,:position,:ad_type,:ad_code,1,NOW(),NOW())'); | ||
| $stmt->execute([ | ||
| 'name' => $_POST['name'], | ||
| 'position' => $_POST['position'], | ||
| 'ad_type' => $_POST['ad_type'], | ||
| 'ad_code' => $_POST['ad_code'], | ||
| ]); | ||
| $message = 'Đã thêm quảng cáo'; | ||
| } | ||
| } | ||
|
Comment on lines
+12
to
+27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CSRF validation doesn't prevent action execution on failure. The current logic validates CSRF but doesn't prevent code from continuing when validation fails. If CSRF fails, the 🔒 Proposed fix with proper validation flow if ($_SERVER['REQUEST_METHOD'] === 'POST' && validate_csrf($_POST['csrf_token'] ?? null)) {
- if (($_POST['action'] ?? '') === 'import_ophim') {
- include __DIR__ . '/../crawler/ophim_crawler.php';
- $message = 'Đã chạy crawler OPhim.';
- }
- if (($_POST['action'] ?? '') === 'add_ad') {
- $stmt = $pdo->prepare('INSERT INTO ads (name, position, ad_type, ad_code, status, created_at, updated_at) VALUES (:name,:position,:ad_type,:ad_code,1,NOW(),NOW())');
- $stmt->execute([
- 'name' => $_POST['name'],
- 'position' => $_POST['position'],
- 'ad_type' => $_POST['ad_type'],
- 'ad_code' => $_POST['ad_code'],
- ]);
- $message = 'Đã thêm quảng cáo';
+ $action = $_POST['action'] ?? '';
+ try {
+ if ($action === 'import_ophim') {
+ include __DIR__ . '/../crawler/ophim_crawler.php';
+ $message = 'Đã chạy crawler OPhim.';
+ } elseif ($action === 'add_ad') {
+ $allowedPositions = ['header', 'sidebar', 'popup', 'video_preroll', 'footer'];
+ $allowedTypes = ['adsense', 'custom_html', 'script'];
+ if (!in_array($_POST['position'], $allowedPositions, true) ||
+ !in_array($_POST['ad_type'], $allowedTypes, true)) {
+ $message = 'Giá trị không hợp lệ.';
+ } else {
+ $stmt = $pdo->prepare('INSERT INTO ads (name, position, ad_type, ad_code, status, created_at, updated_at) VALUES (:name,:position,:ad_type,:ad_code,1,NOW(),NOW())');
+ $stmt->execute([
+ 'name' => $_POST['name'],
+ 'position' => $_POST['position'],
+ 'ad_type' => $_POST['ad_type'],
+ 'ad_code' => $_POST['ad_code'],
+ ]);
+ $message = 'Đã thêm quảng cáo';
+ }
+ }
+ } catch (PDOException $e) {
+ $message = 'Lỗi cơ sở dữ liệu.';
}
+} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ $message = 'CSRF token không hợp lệ.';
}🤖 Prompt for AI Agents |
||
|
|
||
| $movies = $pdo->query('SELECT * FROM movies ORDER BY id DESC LIMIT 20')->fetchAll(); | ||
| $users = $pdo->query('SELECT id,name,email,status FROM users ORDER BY id DESC LIMIT 20')->fetchAll(); | ||
| $comments = $pdo->query('SELECT c.id,c.content,m.title FROM comments c JOIN movies m ON m.id=c.movie_id ORDER BY c.id DESC LIMIT 20')->fetchAll(); | ||
| $ads = $pdo->query('SELECT * FROM ads ORDER BY id DESC')->fetchAll(); | ||
| ?> | ||
| <!doctype html><html><head><meta charset="utf-8"><title>Admin - GENZMOVIE</title><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"></head> | ||
| <body><div class="container py-4"> | ||
| <h2>Admin Dashboard</h2> | ||
| <?php if ($message): ?><div class="alert alert-success"><?= e($message) ?></div><?php endif; ?> | ||
| <div class="row g-3"> | ||
| <div class="col-md-6"><div class="card"><div class="card-body"><h5>Crawler Control</h5><form method="POST"><?= csrf_field() ?><input type="hidden" name="action" value="import_ophim"><button class="btn btn-primary">Import từ OPhim</button></form><p class="mt-2 small">Cronjob: <code>*/30 * * * * php /path/genzmovie/crawler/ophim_crawler.php</code></p></div></div></div> | ||
| <div class="col-md-6"><div class="card"><div class="card-body"><h5>Ads Management</h5><form method="POST"><?= csrf_field() ?><input type="hidden" name="action" value="add_ad"><input class="form-control mb-2" name="name" placeholder="Tên quảng cáo" required><select class="form-select mb-2" name="position"><option>header</option><option>sidebar</option><option>popup</option><option>video_preroll</option><option>footer</option></select><select class="form-select mb-2" name="ad_type"><option>adsense</option><option>custom_html</option><option>script</option></select><textarea class="form-control mb-2" name="ad_code" rows="3" required></textarea><button class="btn btn-danger">Lưu quảng cáo</button></form></div></div></div> | ||
| </div> | ||
| <hr> | ||
| <h5>Movie Management</h5><table class="table table-striped"><tr><th>ID</th><th>Title</th><th>Year</th></tr><?php foreach($movies as $m): ?><tr><td><?= $m['id'] ?></td><td><?= e($m['title']) ?></td><td><?= e((string)$m['year']) ?></td></tr><?php endforeach; ?></table> | ||
| <h5>User Management</h5><table class="table table-striped"><tr><th>ID</th><th>Name</th><th>Email</th><th>Status</th></tr><?php foreach($users as $u): ?><tr><td><?= $u['id'] ?></td><td><?= e($u['name']) ?></td><td><?= e($u['email']) ?></td><td><?= e($u['status']) ?></td></tr><?php endforeach; ?></table> | ||
| <h5>Comment Management</h5><table class="table table-striped"><tr><th>ID</th><th>Movie</th><th>Comment</th></tr><?php foreach($comments as $c): ?><tr><td><?= $c['id'] ?></td><td><?= e($c['title']) ?></td><td><?= e($c['content']) ?></td></tr><?php endforeach; ?></table> | ||
| <h5>Ads</h5><table class="table table-striped"><tr><th>Name</th><th>Position</th><th>Type</th></tr><?php foreach($ads as $ad): ?><tr><td><?= e($ad['name']) ?></td><td><?= e($ad['position']) ?></td><td><?= e($ad['ad_type']) ?></td></tr><?php endforeach; ?></table> | ||
| </div></body></html> | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,28 @@ | ||||||||||||||||||||||||
| <?php | ||||||||||||||||||||||||
| require_once __DIR__ . '/../config/config.php'; | ||||||||||||||||||||||||
| require_once __DIR__ . '/../config/database.php'; | ||||||||||||||||||||||||
| require_once __DIR__ . '/../includes/helpers.php'; | ||||||||||||||||||||||||
| require_once __DIR__ . '/../includes/csrf.php'; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| $error = null; | ||||||||||||||||||||||||
| if ($_SERVER['REQUEST_METHOD'] === 'POST') { | ||||||||||||||||||||||||
| if (!validate_csrf($_POST['csrf_token'] ?? null)) { | ||||||||||||||||||||||||
| $error = 'CSRF token invalid.'; | ||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||
| $stmt = Database::connection()->prepare('SELECT * FROM admins WHERE email = :email LIMIT 1'); | ||||||||||||||||||||||||
| $stmt->execute(['email' => trim($_POST['email'])]); | ||||||||||||||||||||||||
| $admin = $stmt->fetch(); | ||||||||||||||||||||||||
| if ($admin && password_verify($_POST['password'], $admin['password'])) { | ||||||||||||||||||||||||
| $_SESSION['admin'] = $admin; | ||||||||||||||||||||||||
| header('Location: /genzmovie/admin/index.php'); | ||||||||||||||||||||||||
|
Comment on lines
+15
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Find every consumer of the admin session payload.
rg -n -C2 -g '!vendor/**' "\$_SESSION\['admin'\]|\$_SESSION\[\"admin\"\]"
# Check whether any auth flow already rotates the session ID.
rg -n -C2 -g '!vendor/**' 'session_regenerate_id\s*\('Repository: huzely/Decrypt Length of output: 40 🏁 Script executed: # List the repository structure
git ls-files | head -30
# Check if genzmovie directory exists and explore it
find . -type d -name "genzmovie" -o -type d -name "admin" | head -20Repository: huzely/Decrypt Length of output: 959 🏁 Script executed: # Read the login.php file to see lines 15-17 and context
cat -n genzmovie/admin/login.php
# Search with broader patterns for session usage
rg -n "SESSION.*admin|admin.*SESSION" -g '!vendor/**'
# Check for session_regenerate_id anywhere
rg -n "session_regenerate_id" -g '!vendor/**'
# Search in auth.php as well
cat -n genzmovie/includes/auth.phpRepository: huzely/Decrypt Length of output: 2293 🏁 Script executed: # Check database schema to see what fields the admins table has
cat -n genzmovie/database/schema.sql | grep -A 20 "CREATE TABLE.*admins"
# Check how admin/index.php uses the session data
cat -n genzmovie/admin/index.phpRepository: huzely/Decrypt Length of output: 5131 Rotate the session and store only minimal admin claims. Successful login writes the full admin row into the existing session, keeping the password hash in session state and leaving the pre-auth session ID valid after privilege escalation. The admins table includes a 🔒 Minimal hardening change if ($admin && password_verify($_POST['password'], $admin['password'])) {
+ session_regenerate_id(true);
- $_SESSION['admin'] = $admin;
+ $_SESSION['admin'] = [
+ 'id' => (int) $admin['id'],
+ 'email' => $admin['email'],
+ ];
header('Location: /genzmovie/admin/index.php');
exit;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| exit; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| $error = 'Sai thông tin đăng nhập'; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| ?> | ||||||||||||||||||||||||
| <!doctype html><html><head><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"></head> | ||||||||||||||||||||||||
| <body class="bg-dark text-light d-flex align-items-center" style="height:100vh"><div class="container"><div class="col-md-4 mx-auto"> | ||||||||||||||||||||||||
| <h3>Admin Login</h3><?php if ($error): ?><div class="alert alert-danger"><?= e($error) ?></div><?php endif; ?> | ||||||||||||||||||||||||
| <form method="POST"><?= csrf_field() ?><input class="form-control mb-2" name="email" type="email" required><input class="form-control mb-2" name="password" type="password" required><button class="btn btn-danger w-100">Login</button></form> | ||||||||||||||||||||||||
| </div></div></body></html> | ||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,16 @@ | ||||||||||||||||||||||||
| <?php | ||||||||||||||||||||||||
| require_once __DIR__ . '/../config/config.php'; | ||||||||||||||||||||||||
| require_once __DIR__ . '/../config/database.php'; | ||||||||||||||||||||||||
| header('Content-Type: application/xml; charset=utf-8'); | ||||||||||||||||||||||||
| $movies = Database::connection()->query('SELECT slug, updated_at FROM movies ORDER BY updated_at DESC')->fetchAll(); | ||||||||||||||||||||||||
| echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"; | ||||||||||||||||||||||||
| ?> | ||||||||||||||||||||||||
| <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> | ||||||||||||||||||||||||
| <url><loc><?= BASE_URL ?>/</loc></url> | ||||||||||||||||||||||||
| <?php foreach ($movies as $movie): ?> | ||||||||||||||||||||||||
| <url> | ||||||||||||||||||||||||
| <loc><?= BASE_URL ?>/phim/<?= htmlspecialchars($movie['slug']) ?></loc> | ||||||||||||||||||||||||
| <lastmod><?= date('c', strtotime($movie['updated_at'])) ?></lastmod> | ||||||||||||||||||||||||
| </url> | ||||||||||||||||||||||||
|
Comment on lines
+10
to
+14
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle null If 🛠️ Proposed fix <?php foreach ($movies as $movie): ?>
<url>
<loc><?= BASE_URL ?>/phim/<?= htmlspecialchars($movie['slug']) ?></loc>
- <lastmod><?= date('c', strtotime($movie['updated_at'])) ?></lastmod>
+ <lastmod><?= $movie['updated_at'] ? date('c', strtotime($movie['updated_at'])) : date('c') ?></lastmod>
</url>
<?php endforeach; ?>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| <?php endforeach; ?> | ||||||||||||||||||||||||
| </urlset> | ||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| body { font-family: 'Segoe UI', sans-serif; } | ||
| .movie-card img { height: 260px; object-fit: cover; } | ||
| .movie-card:hover { transform: translateY(-4px); transition: .2s; box-shadow: 0 0 12px rgba(255,255,255,.12); } | ||
| .featured-img { max-height: 420px; object-fit: cover; filter: brightness(.6); } | ||
| .bg-overlay { background: rgba(0,0,0,.55); } | ||
| .autocomplete-box { position:absolute; top:100%; left:0; right:0; background:#111; z-index:1000; border:1px solid #444; } | ||
| .autocomplete-box a { display:block; color:#fff; padding:6px 10px; text-decoration:none; } | ||
| .autocomplete-box a:hover { background:#222; } | ||
| .ad-slot iframe, .ad-slot img { max-width: 100%; } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| $(function () { | ||
| const $search = $('#movie-search'); | ||
| const $box = $('#autocomplete-box'); | ||
|
|
||
| $search.on('input', function () { | ||
| const q = $(this).val().trim(); | ||
| if (q.length < 2) { | ||
| $box.empty(); | ||
| return; | ||
| } | ||
| $.getJSON('/genzmovie/?route=autocomplete&q=' + encodeURIComponent(q), function (items) { | ||
| $box.empty(); | ||
| items.forEach(item => { | ||
| $box.append(`<a href="/genzmovie/?route=movie&slug=${item.slug}">${item.title}</a>`); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The autocomplete renderer appends Useful? React with 👍 / 👎. |
||
| }); | ||
| }); | ||
|
Comment on lines
+11
to
+16
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. XSS vulnerability and missing debounce. The response data ( 🔒 Proposed fix with escaping and debounce $(function () {
const $search = $('#movie-search');
const $box = $('#autocomplete-box');
+ let debounceTimer;
+
+ function escapeHtml(str) {
+ const div = document.createElement('div');
+ div.textContent = str;
+ return div.innerHTML;
+ }
$search.on('input', function () {
+ clearTimeout(debounceTimer);
const q = $(this).val().trim();
if (q.length < 2) {
$box.empty();
return;
}
- $.getJSON('/genzmovie/?route=autocomplete&q=' + encodeURIComponent(q), function (items) {
- $box.empty();
- items.forEach(item => {
- $box.append(`<a href="/genzmovie/?route=movie&slug=${item.slug}">${item.title}</a>`);
+ debounceTimer = setTimeout(function () {
+ $.getJSON('/genzmovie/?route=autocomplete&q=' + encodeURIComponent(q), function (items) {
+ $box.empty();
+ items.forEach(item => {
+ $box.append(`<a href="/genzmovie/?route=movie&slug=${encodeURIComponent(item.slug)}">${escapeHtml(item.title)}</a>`);
+ });
+ }).fail(function () {
+ $box.empty();
});
- });
+ }, 300);
});🤖 Prompt for AI Agents |
||
| }); | ||
|
|
||
| $(document).on('click', function (e) { | ||
| if (!$(e.target).closest('#movie-search').length) { | ||
| $box.empty(); | ||
| } | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| const DB_HOST = '127.0.0.1'; | ||
| const DB_NAME = 'genzmovie'; | ||
| const DB_USER = 'root'; | ||
| const DB_PASS = ''; | ||
| const BASE_URL = 'http://localhost/genzmovie'; | ||
| const APP_NAME = 'GENZMOVIE'; | ||
| const OPHIM_API_BASE = 'https://ophim1.com/v1/api'; | ||
|
Comment on lines
+5
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Move credentials and deployment URLs out of source control. The scaffold ships 🤖 Prompt for AI Agents |
||
|
|
||
| if (session_status() !== PHP_SESSION_ACTIVE) { | ||
| session_start(); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| require_once __DIR__ . '/config.php'; | ||
|
|
||
| class Database | ||
| { | ||
| private static ?PDO $instance = null; | ||
|
|
||
| public static function connection(): PDO | ||
| { | ||
| if (self::$instance === null) { | ||
| $dsn = sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', DB_HOST, DB_NAME); | ||
| self::$instance = new PDO($dsn, DB_USER, DB_PASS, [ | ||
| PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, | ||
| PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, | ||
| PDO::ATTR_EMULATE_PREPARES => false, | ||
| ]); | ||
| } | ||
|
|
||
| return self::$instance; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| require_once __DIR__ . '/../models/User.php'; | ||
|
|
||
| class AuthController | ||
| { | ||
| public function login(): void | ||
| { | ||
| $error = null; | ||
| if ($_SERVER['REQUEST_METHOD'] === 'POST') { | ||
| if (!validate_csrf($_POST['csrf_token'] ?? null)) { | ||
| $error = 'CSRF token invalid.'; | ||
| } else { | ||
| $userModel = new User(); | ||
| $user = $userModel->findByEmail(trim($_POST['email'])); | ||
| if ($user && password_verify($_POST['password'], $user['password'])) { | ||
| $_SESSION['user'] = $user; | ||
| redirect('/'); | ||
| } | ||
| $error = 'Email hoặc mật khẩu không đúng.'; | ||
| } | ||
| } | ||
| include __DIR__ . '/../views/auth/login.php'; | ||
| } | ||
|
|
||
| public function register(): void | ||
| { | ||
| $error = null; | ||
| if ($_SERVER['REQUEST_METHOD'] === 'POST') { | ||
| if (!validate_csrf($_POST['csrf_token'] ?? null)) { | ||
| $error = 'CSRF token invalid.'; | ||
| } else { | ||
| $userModel = new User(); | ||
| $ok = $userModel->create(trim($_POST['name']), trim($_POST['email']), $_POST['password']); | ||
| if ($ok) { | ||
| redirect('/?route=login'); | ||
| } | ||
| $error = 'Không thể tạo tài khoản.'; | ||
| } | ||
| } | ||
| include __DIR__ . '/../views/auth/register.php'; | ||
| } | ||
|
|
||
| public function logout(): void | ||
| { | ||
| unset($_SESSION['user']); | ||
| redirect('/'); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| require_once __DIR__ . '/../models/Movie.php'; | ||
|
|
||
| class HomeController | ||
| { | ||
| public function index(): void | ||
| { | ||
| $movieModel = new Movie(); | ||
| $sections = [ | ||
| 'Phim mới cập nhật' => 'series', | ||
| 'Phim chiếu rạp' => 'theater', | ||
| 'Phim bộ' => 'series', | ||
|
Comment on lines
+13
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Both labels map to 🤖 Prompt for AI Agents |
||
| 'Phim lẻ' => 'single', | ||
| 'Phim Netflix đề cử' => 'netflix', | ||
| 'Phim hành động' => 'Hành Động', | ||
| 'Phim kinh dị' => 'Kinh Dị', | ||
| 'Phim hoạt hình' => 'Hoạt Hình', | ||
| ]; | ||
|
|
||
| $featured = $movieModel->featured(); | ||
| $moviesBySection = []; | ||
| foreach ($sections as $label => $key) { | ||
| $moviesBySection[$label] = $movieModel->byCategory($key); | ||
| } | ||
|
|
||
| include __DIR__ . '/../views/home/index.php'; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do not ship publicly documented seeded admin credentials.
Documenting a known default admin account/password here makes accidental insecure deployments much more likely, especially since the PR also seeds that admin in the schema. Please switch installation to require creating/rotating admin credentials during setup, and avoid publishing a reusable password in the README.
Suggested doc change
📝 Committable suggestion
🤖 Prompt for AI Agents