- 134-tool registry with programmatic SEO (unique titles/H1/descriptions, JSON-LD graphs, sitemap index, canonical 301 enforcement via required filter) - DB-backed job queue (SKIP LOCKED) with drivers: Ffmpeg, Images (GD), Pdf (qpdf/gs/poppler), Youtube (thumbnails), Qr (server-side PNG) - Security: SSRF guard, MIME validation, rate limits, API-key auth, bcrypt admin login, security headers - Admin panel: dashboard, tools/categories/guides CRUD, SEO audit, analytics, job inspector with retry, system health, feature flags - Docker deployment (nginx + web/api FPM pools + scalable workers), PHPUnit suite (19 tests / 1139 assertions), PWA manifest + service worker
106 lines
3.1 KiB
PHP
106 lines
3.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Controllers\Admin;
|
|
|
|
use CodeIgniter\HTTP\ResponseInterface;
|
|
|
|
/** Tools CRUD: list, create (with generated SEO content), edit, delete. */
|
|
final class ToolsAdmin extends AdminBase
|
|
{
|
|
public function index(): string
|
|
{
|
|
$q = trim((string) $this->request->getGet('q'));
|
|
|
|
$query = $this->toolsModel()->orderBy('name', 'ASC')->limit(300);
|
|
if ($q !== '') {
|
|
$query->like('name', $q)->orLike('slug', $q);
|
|
}
|
|
|
|
return $this->render('tools_index', [
|
|
'tools' => $query->findAll(),
|
|
'categories' => $this->categories(),
|
|
'q' => $q,
|
|
]);
|
|
}
|
|
|
|
public function create(): string
|
|
{
|
|
return $this->render('tools_form', [
|
|
'tool' => null,
|
|
'categories' => $this->categories(),
|
|
]);
|
|
}
|
|
|
|
public function store(): ResponseInterface
|
|
{
|
|
return $this->save(null);
|
|
}
|
|
|
|
public function edit(int $id): string
|
|
{
|
|
$tool = $this->toolsModel()->find($id) ?? throw new \CodeIgniter\Exceptions\PageNotFoundException();
|
|
|
|
return $this->render('tools_form', [
|
|
'tool' => $tool,
|
|
'categories' => $this->categories(),
|
|
]);
|
|
}
|
|
|
|
public function update(int $id): ResponseInterface
|
|
{
|
|
return $this->save($id);
|
|
}
|
|
|
|
private function save(?int $id): ResponseInterface
|
|
{
|
|
$post = $this->request->getPost();
|
|
$catId = (int) ($post['category_id'] ?? 0);
|
|
|
|
try {
|
|
$row = $this->toolFromInput((array) $post, $catId);
|
|
} catch (\Throwable $e) {
|
|
session()->setFlashdata('error', 'Could not build the tool: ' . $e->getMessage());
|
|
|
|
return redirect()->back()->withInput();
|
|
}
|
|
|
|
// pre-publish SEO validation (critical checks only)
|
|
$problems = [];
|
|
if (trim((string) $row['seo_title']) === '') {
|
|
$problems[] = 'SEO title missing';
|
|
}
|
|
if (mb_strlen((string) $row['seo_description']) < 50) {
|
|
$problems[] = 'SEO description too short (<50 chars)';
|
|
}
|
|
if (trim((string) $row['h1']) === '') {
|
|
$problems[] = 'H1 missing';
|
|
}
|
|
if ($problems !== [] && ($row['status'] ?? '') === 'active') {
|
|
session()->setFlashdata('error', 'Blocked from publishing: ' . implode('; ', $problems));
|
|
session()->setFlashdata('form_data', $post);
|
|
|
|
return redirect()->to($id ? '/admin/tools/' . $id . '/edit' : '/admin/tools/new')->withInput();
|
|
}
|
|
|
|
if ($id !== null) {
|
|
$this->toolsModel()->update($id, $row);
|
|
session()->setFlashdata('success', 'Tool updated.');
|
|
} else {
|
|
$this->toolsModel()->insert($row);
|
|
session()->setFlashdata('success', 'Tool created.');
|
|
}
|
|
|
|
return redirect()->to('/admin/tools');
|
|
}
|
|
|
|
public function delete(int $id): ResponseInterface
|
|
{
|
|
$this->toolsModel()->delete($id);
|
|
service('cache')->clear();
|
|
|
|
return redirect()->to('/admin/tools');
|
|
}
|
|
}
|