Sách Giáo Khoa Việt Nam - sachgiaokhoa.org

CodeIgniter 4 website:
- Catalog 999 SGK (lớp 1-12) từ API, lọc theo lớp/môn/NXB, tìm kiếm
- Trang chủ grouped browse (bậc học -> lớp -> môn, ưu tiên môn quan trọng)
- Chi tiết sách: metadata đầy đủ, cover nghệ thuật /thumb/{W}x{H}/
- Đọc online qua gate captcha + countdown, PDF viewer pdf.js
- Download: captcha + token một lần + auto cache-bust version
- Tự đồng bộ catalog/cover khi API thay đổi (refresh:catalog)
- Legal: DMCA, miễn trừ trách nhiệm, điều khoản, bảo mật, liên hệ
This commit is contained in:
sachgiaokhoaorg
2026-08-23 07:05:49 +00:00
commit e6b265db99
494 changed files with 134829 additions and 0 deletions
+132
View File
@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Libraries\BookApi;
use App\Libraries\Captcha;
use App\Libraries\Mongo;
use App\Libraries\PdfAccess;
/**
* Access gate: captcha + countdown before viewing/downloading a book's PDF.
*/
class Download extends BaseController
{
private BookApi $api;
public function __construct()
{
$this->api = new BookApi();
helper(['text', 'form']);
}
/**
* Reader route /doc/{id}: captcha gate first, then the reading page.
*/
public function page(string $id)
{
$book = $this->api->book($id);
if (! $book) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
$error = null;
if ($this->request->getPost('download')) {
if (Captcha::verify($this->request->getPost('captcha'))) {
PdfAccess::grant($id);
return redirect()->to(site_url('doc/' . $id));
}
$error = 'Mã captcha không đúng hoặc đã hết hạn. Vui lòng thử lại.';
}
// access granted → reading page with viewer
if (PdfAccess::has($id)) {
return view('books/read', ['book' => $book]);
}
// otherwise → captcha gate
return view('books/download', [
'book' => $book,
'error' => $error,
'countdown' => (int) env('download.countdown', 15),
]);
}
public function captcha()
{
Captcha::render(Captcha::issue());
}
/**
* Token-gated actual download. Requires an active PDF access grant.
*/
public function fetch(string $id, string $token)
{
if (! PdfAccess::has($id)) {
return view('pages/error_api', ['message' => 'Phiên truy cập đã hết hạn. Vui lòng xác nhận captcha lại để tải xuống.']);
}
if (! $this->consumeToken($id, $token)) {
return view('pages/error_api', ['message' => 'Liên kết tải về không hợp lệ hoặc đã hết hạn. Vui lòng thực hiện lại quy trình tải xuống.']);
}
$book = $this->api->book($id);
if (! $book) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
// stats
$stats = Mongo::col('stats');
$stats->updateOne(['_id' => 'book:' . $id], ['$inc' => ['downloads' => 1]], ['upsert' => true]);
$stats->updateOne(['_id' => 'global'], ['$inc' => ['downloads' => 1]], ['upsert' => true]);
$safeName = preg_replace('/[^\p{L}\p{N} _.-]/u', '_', $book['title']) . '.pdf';
$this->response->setHeader('Content-Type', 'application/pdf');
$this->api->stream($book['pdfEndpoint'], $safeName);
exit;
}
/**
* Issue a one-time download token (requires access grant).
*/
public function token(string $id)
{
if (! $this->request->isAJAX() && strtolower($this->request->getMethod()) !== 'post') {
return $this->response->setStatusCode(405);
}
if (! PdfAccess::has($id)) {
return $this->response->setStatusCode(403)->setJSON(['ok' => false, 'error' => 'expired']);
}
$token = bin2hex(random_bytes(16));
Mongo::col('download_tokens')->insertOne([
'_id' => $token,
'bookId' => $id,
'session' => session_id(),
'expiresAt' => new \MongoDB\BSON\UTCDateTime((time() + 300) * 1000),
'used' => false,
]);
return $this->response->setJSON(['ok' => true, 'url' => site_url("tai-ve/{$id}/{$token}")]);
}
private function consumeToken(string $id, string $token): bool
{
$col = Mongo::col('download_tokens');
$doc = $col->findOne(['_id' => $token, 'bookId' => $id, 'used' => false]);
if (! $doc) {
return false;
}
if ($doc['expiresAt']->toDateTime()->getTimestamp() < time()) {
$col->deleteOne(['_id' => $token]);
return false;
}
$col->updateOne(['_id' => $token], ['$set' => ['used' => true]]);
return true;
}
}