Initial commit - CMS Gov Bapenda Garut dengan EditorJS

This commit is contained in:
2026-01-05 06:47:36 +07:00
commit bd649bd5f2
634 changed files with 215640 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
<?php
namespace App\Controllers\Admin;
use App\Controllers\BaseController;
use App\Models\AuditLogModel;
use App\Models\UserModel;
class AuditLogs extends BaseController
{
protected $auditLogModel;
protected $userModel;
public function __construct()
{
$this->auditLogModel = new AuditLogModel();
$this->userModel = new UserModel();
}
/**
* Display audit logs with pagination
*/
public function index()
{
// Check if user is admin
if (session()->get('role') !== 'admin') {
return redirect()->to('/admin/dashboard')
->with('error', 'Anda tidak memiliki akses ke halaman ini.');
}
// Get search query
$search = $this->request->getGet('search');
$actionFilter = $this->request->getGet('action');
$userFilter = $this->request->getGet('user');
// Build query
$this->auditLogModel->select('audit_logs.*, users.username, users.email')
->join('users', 'users.id = audit_logs.user_id', 'left')
->orderBy('audit_logs.created_at', 'DESC');
// Apply search filter
if (!empty($search)) {
$this->auditLogModel->groupStart()
->like('audit_logs.action', $search)
->orLike('users.username', $search)
->orLike('users.email', $search)
->orLike('audit_logs.ip_address', $search)
->groupEnd();
}
// Apply action filter
if (!empty($actionFilter)) {
$this->auditLogModel->where('audit_logs.action', $actionFilter);
}
// Apply user filter
if (!empty($userFilter)) {
$this->auditLogModel->where('audit_logs.user_id', $userFilter);
}
// Get paginated results
$perPage = 20;
$page = (int) ($this->request->getGet('page') ?? 1);
$auditLogs = $this->auditLogModel->paginate($perPage, 'default', $page);
$pager = $this->auditLogModel->pager;
$total = $pager->getTotal();
// Get unique actions for filter dropdown
$actions = $this->auditLogModel->select('action')
->distinct()
->orderBy('action', 'ASC')
->findAll();
// Get users for filter dropdown
$users = $this->userModel->select('id, username, email')
->orderBy('username', 'ASC')
->findAll();
$data = [
'title' => 'Audit Log',
'auditLogs' => $auditLogs,
'pager' => $pager,
'search' => $search,
'actionFilter' => $actionFilter,
'userFilter' => $userFilter,
'actions' => $actions,
'users' => $users,
'total' => $total,
];
return view('admin/audit-logs/index', $data);
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Controllers\Admin;
use App\Controllers\BaseController;
use App\Models\NewsModel;
use App\Models\UserModel;
use App\Models\AuditLogModel;
class Dashboard extends BaseController
{
protected $newsModel;
protected $userModel;
protected $auditLogModel;
public function __construct()
{
$this->newsModel = new NewsModel();
$this->userModel = new UserModel();
$this->auditLogModel = new AuditLogModel();
}
public function index()
{
// Get news statistics
$totalNews = $this->newsModel->countByStatus();
$publishedNews = $this->newsModel->countByStatus('published');
$draftNews = $this->newsModel->countByStatus('draft');
// Get pages statistics (query directly since no PageModel)
$db = \Config\Database::connect();
$totalPages = $db->table('pages')->countAllResults();
$publishedPages = $db->table('pages')->where('status', 'published')->countAllResults();
$draftPages = $db->table('pages')->where('status', 'draft')->countAllResults();
// Get users statistics
$totalUsers = $this->userModel->countAllResults();
$activeUsers = $this->userModel->where('is_active', 1)->countAllResults();
// Get recent audit logs (limit 10)
$recentAuditLogs = $this->auditLogModel->select('audit_logs.*, users.username')
->join('users', 'users.id = audit_logs.user_id', 'left')
->orderBy('audit_logs.created_at', 'DESC')
->limit(10)
->findAll();
$data = [
'title' => 'Dashboard',
'stats' => [
'news' => [
'total' => $totalNews,
'published' => $publishedNews,
'draft' => $draftNews,
],
'pages' => [
'total' => $totalPages,
'published' => $publishedPages,
'draft' => $draftPages,
],
'users' => [
'total' => $totalUsers,
'active' => $activeUsers,
],
],
'recentAuditLogs' => $recentAuditLogs,
];
return view('admin/dashboard', $data);
}
}

View File

@@ -0,0 +1,281 @@
<?php
namespace App\Controllers\Admin;
use App\Controllers\BaseController;
use App\Models\NewsModel;
use App\Models\AuditLogModel;
class News extends BaseController
{
protected $newsModel;
protected $auditLogModel;
public function __construct()
{
$this->newsModel = new NewsModel();
$this->auditLogModel = new AuditLogModel();
}
/**
* Display list of news
*/
public function index()
{
$perPage = 10;
$page = $this->request->getGet('page') ?? 1;
$status = $this->request->getGet('status');
$search = $this->request->getGet('search');
// Build query with filters
$this->newsModel->select('news.*, users.username as creator_name')
->join('users', 'users.id = news.created_by', 'left');
// Filter by status
if ($status && in_array($status, ['draft', 'published'])) {
$this->newsModel->where('news.status', $status);
}
// Search
if ($search) {
$this->newsModel->groupStart()
->like('news.title', $search)
->orLike('news.content', $search)
->groupEnd();
}
// Get paginated results
$news = $this->newsModel->orderBy('news.created_at', 'DESC')
->paginate($perPage, 'default', $page);
$pager = $this->newsModel->pager;
$data = [
'title' => 'Berita',
'news' => $news,
'pager' => $pager,
'currentStatus' => $status,
'currentSearch' => $search,
'stats' => [
'total' => $this->newsModel->countByStatus(),
'published' => $this->newsModel->countByStatus('published'),
'draft' => $this->newsModel->countByStatus('draft'),
],
];
return view('admin/news/index', $data);
}
/**
* Show form to create new news
*/
public function create()
{
$data = [
'title' => 'Tambah Berita',
'news' => null,
];
return view('admin/news/form', $data);
}
/**
* Store new news
*/
public function store()
{
$validation = \Config\Services::validation();
$rules = [
'title' => 'required|min_length[3]|max_length[255]',
'content' => 'required',
'status' => 'required|in_list[draft,published]',
];
if (!$this->validate($rules)) {
return redirect()->back()
->withInput()
->with('validation', $validation);
}
$title = $this->request->getPost('title');
$slug = $this->newsModel->generateSlug($title);
$content = $this->request->getPost('content');
$contentHtml = $this->request->getPost('content_html');
$contentJson = $this->request->getPost('content_json');
$excerpt = $this->request->getPost('excerpt');
$status = $this->request->getPost('status');
$userId = session()->get('user_id');
// Use content_html if available, otherwise use content
$finalContent = !empty($contentHtml) ? $contentHtml : $content;
$data = [
'title' => $title,
'slug' => $slug,
'content' => $finalContent,
'content_html' => $contentHtml,
'content_json' => $contentJson,
'excerpt' => $excerpt,
'status' => $status,
'created_by' => $userId,
];
// Set published_at if status is published
if ($status === 'published') {
$data['published_at'] = date('Y-m-d H:i:s');
}
if ($this->newsModel->insert($data)) {
// Log action
$this->auditLogModel->logAction('news_created', $userId);
return redirect()->to('/admin/news')
->with('success', 'Berita berhasil ditambahkan.');
}
return redirect()->back()
->withInput()
->with('error', 'Gagal menambahkan berita.');
}
/**
* Show form to edit news
*/
public function edit($id)
{
$news = $this->newsModel->find($id);
if (!$news) {
return redirect()->to('/admin/news')
->with('error', 'Berita tidak ditemukan.');
}
$data = [
'title' => 'Edit Berita',
'news' => $news,
];
return view('admin/news/form', $data);
}
/**
* Update news
*/
public function update($id)
{
$news = $this->newsModel->find($id);
if (!$news) {
return redirect()->to('/admin/news')
->with('error', 'Berita tidak ditemukan.');
}
$validation = \Config\Services::validation();
$rules = [
'title' => 'required|min_length[3]|max_length[255]',
'content' => 'required',
'status' => 'required|in_list[draft,published]',
];
if (!$this->validate($rules)) {
return redirect()->back()
->withInput()
->with('validation', $validation);
}
$title = $this->request->getPost('title');
$oldTitle = $news['title'];
$content = $this->request->getPost('content');
$contentHtml = $this->request->getPost('content_html');
$contentJson = $this->request->getPost('content_json');
$excerpt = $this->request->getPost('excerpt');
$status = $this->request->getPost('status');
$userId = session()->get('user_id');
// Use content_html if available, otherwise use content
$finalContent = !empty($contentHtml) ? $contentHtml : $content;
// Generate new slug if title changed
$slug = ($title !== $oldTitle)
? $this->newsModel->generateSlug($title, $id)
: $news['slug'];
$data = [
'title' => $title,
'slug' => $slug,
'content' => $finalContent,
'content_html' => $contentHtml,
'content_json' => $contentJson,
'excerpt' => $excerpt,
'status' => $status,
];
// Set published_at if status changed to published and wasn't published before
if ($status === 'published' && empty($news['published_at'])) {
$data['published_at'] = date('Y-m-d H:i:s');
}
try {
// Skip model validation karena sudah divalidasi di controller
$this->newsModel->skipValidation(true);
$result = $this->newsModel->update($id, $data);
if ($result === false) {
// Get validation errors if any
$errors = $this->newsModel->errors();
$errorMessage = !empty($errors)
? implode(', ', $errors)
: 'Gagal memperbarui berita.';
log_message('error', 'News update failed - ID: ' . $id . ', Errors: ' . json_encode($errors));
return redirect()->back()
->withInput()
->with('error', $errorMessage);
}
// Log action
$this->auditLogModel->logAction('news_updated', $userId);
return redirect()->to('/admin/news')
->with('success', 'Berita berhasil diperbarui.');
} catch (\Exception $e) {
log_message('error', 'News update exception - ID: ' . $id . ', Error: ' . $e->getMessage() . ' | Trace: ' . $e->getTraceAsString());
return redirect()->back()
->withInput()
->with('error', 'Terjadi kesalahan saat memperbarui berita: ' . $e->getMessage());
}
}
/**
* Delete news
*/
public function delete($id)
{
$news = $this->newsModel->find($id);
if (!$news) {
return redirect()->to('/admin/news')
->with('error', 'Berita tidak ditemukan.');
}
$userId = session()->get('user_id');
if ($this->newsModel->delete($id)) {
// Log action
$this->auditLogModel->logAction('news_deleted', $userId);
return redirect()->to('/admin/news')
->with('success', 'Berita berhasil dihapus.');
}
return redirect()->to('/admin/news')
->with('error', 'Gagal menghapus berita.');
}
}

View File

@@ -0,0 +1,442 @@
<?php
namespace App\Controllers\Admin;
use App\Controllers\BaseController;
use App\Models\PageModel;
use App\Models\AuditLogModel;
use App\Services\ContentRenderer;
class Pages extends BaseController
{
protected $pageModel;
protected $auditLogModel;
public function __construct()
{
$this->pageModel = new PageModel();
$this->auditLogModel = new AuditLogModel();
}
/**
* Display list of pages
*/
public function index()
{
$perPage = 10;
$page = $this->request->getGet('page') ?? 1;
$status = $this->request->getGet('status');
$search = $this->request->getGet('search');
// Build query with filters
$this->pageModel->select('pages.*');
// Filter by status
if ($status && in_array($status, ['draft', 'published'])) {
$this->pageModel->where('pages.status', $status);
}
// Search
if ($search) {
$this->pageModel->groupStart()
->like('pages.title', $search)
->orLike('pages.content_html', $search)
->orLike('pages.excerpt', $search)
->groupEnd();
}
// Get paginated results
$pages = $this->pageModel->orderBy('pages.created_at', 'DESC')
->paginate($perPage, 'default', $page);
$pager = $this->pageModel->pager;
$data = [
'title' => 'Halaman',
'pages' => $pages,
'pager' => $pager,
'currentStatus' => $status,
'currentSearch' => $search,
'stats' => [
'total' => $this->pageModel->countByStatus(),
'published' => $this->pageModel->countByStatus('published'),
'draft' => $this->pageModel->countByStatus('draft'),
],
];
return view('admin/pages/index', $data);
}
/**
* Show form to create new page
*/
public function create()
{
$data = [
'title' => 'Tambah Halaman',
'page' => null,
];
return view('admin/pages/form', $data);
}
/**
* Store new page
*/
public function store()
{
$validation = \Config\Services::validation();
$rules = [
'title' => 'required|min_length[3]|max_length[255]',
'content_json' => 'permit_empty',
'status' => 'required|in_list[draft,published]',
];
if (!$this->validate($rules)) {
return redirect()->back()
->withInput()
->with('validation', $validation);
}
$title = $this->request->getPost('title');
$slug = $this->pageModel->generateSlug($title);
$contentJson = $this->request->getPost('content_json') ?? '{}';
$contentHtml = $this->request->getPost('content_html') ?? '';
$excerpt = $this->request->getPost('excerpt') ?? '';
$featuredImage = $this->request->getPost('featured_image') ?? null;
$status = $this->request->getPost('status');
$userId = session()->get('user_id');
// Validate and parse JSON
$blocks = [];
if (!empty($contentJson)) {
$parsed = json_decode($contentJson, true);
if (json_last_error() === JSON_ERROR_NONE && isset($parsed['blocks'])) {
$blocks = $parsed['blocks'];
}
}
// Render HTML from JSON if not provided
if (empty($contentHtml) && !empty($blocks)) {
$contentHtml = ContentRenderer::renderEditorJsToHtml($blocks);
}
// Sanitize HTML
$contentHtml = $this->sanitizeHtml($contentHtml);
// Extract excerpt if empty
if (empty($excerpt) && !empty($blocks)) {
$excerpt = ContentRenderer::extractExcerpt($blocks);
}
$data = [
'title' => $title,
'slug' => $slug,
'content' => $contentHtml, // Keep for backward compatibility
'content_json' => $contentJson,
'content_html' => $contentHtml,
'excerpt' => $excerpt,
'featured_image' => $featuredImage,
'status' => $status,
];
if ($this->pageModel->insert($data)) {
// Log action
$this->auditLogModel->logAction('page_created', $userId);
return redirect()->to('/admin/pages')
->with('success', 'Halaman berhasil ditambahkan.');
}
return redirect()->back()
->withInput()
->with('error', 'Gagal menambahkan halaman.');
}
/**
* Show form to edit page
*/
public function edit($id)
{
$page = $this->pageModel->find($id);
if (!$page) {
return redirect()->to('/admin/pages')
->with('error', 'Halaman tidak ditemukan.');
}
$data = [
'title' => 'Edit Halaman',
'page' => $page,
];
return view('admin/pages/form', $data);
}
/**
* Update page
*/
public function update($id)
{
$page = $this->pageModel->find($id);
if (!$page) {
return redirect()->to('/admin/pages')
->with('error', 'Halaman tidak ditemukan.');
}
$validation = \Config\Services::validation();
$rules = [
'title' => 'required|min_length[3]|max_length[255]',
'content_json' => 'permit_empty',
'status' => 'required|in_list[draft,published]',
];
if (!$this->validate($rules)) {
return redirect()->back()
->withInput()
->with('validation', $validation);
}
$title = $this->request->getPost('title');
$oldTitle = $page['title'];
$contentJson = $this->request->getPost('content_json') ?? '{}';
$contentHtml = $this->request->getPost('content_html') ?? '';
$excerpt = $this->request->getPost('excerpt') ?? '';
$featuredImage = $this->request->getPost('featured_image') ?? null;
$status = $this->request->getPost('status');
$userId = session()->get('user_id');
// Validate and parse JSON
$blocks = [];
if (!empty($contentJson)) {
$parsed = json_decode($contentJson, true);
if (json_last_error() === JSON_ERROR_NONE && isset($parsed['blocks'])) {
$blocks = $parsed['blocks'];
}
}
// Render HTML from JSON if not provided
if (empty($contentHtml) && !empty($blocks)) {
$contentHtml = ContentRenderer::renderEditorJsToHtml($blocks);
}
// Sanitize HTML
$contentHtml = $this->sanitizeHtml($contentHtml);
// Extract excerpt if empty
if (empty($excerpt) && !empty($blocks)) {
$excerpt = ContentRenderer::extractExcerpt($blocks);
}
// Generate new slug if title changed
$slug = ($title !== $oldTitle)
? $this->pageModel->generateSlug($title, $id)
: $page['slug'];
$data = [
'title' => $title,
'slug' => $slug,
'content' => $contentHtml, // Keep for backward compatibility
'content_json' => $contentJson,
'content_html' => $contentHtml,
'excerpt' => $excerpt,
'featured_image' => $featuredImage,
'status' => $status,
];
try {
$this->pageModel->skipValidation(true);
$result = $this->pageModel->update($id, $data);
if ($result === false) {
$errors = $this->pageModel->errors();
$errorMessage = !empty($errors)
? implode(', ', $errors)
: 'Gagal memperbarui halaman.';
log_message('error', 'Page update failed - ID: ' . $id . ', Errors: ' . json_encode($errors));
return redirect()->back()
->withInput()
->with('error', $errorMessage);
}
// Log action
$this->auditLogModel->logAction('page_updated', $userId);
return redirect()->to('/admin/pages')
->with('success', 'Halaman berhasil diperbarui.');
} catch (\Exception $e) {
log_message('error', 'Page update exception - ID: ' . $id . ', Error: ' . $e->getMessage());
return redirect()->back()
->withInput()
->with('error', 'Terjadi kesalahan saat memperbarui halaman: ' . $e->getMessage());
}
}
/**
* Autosave page (AJAX)
*/
public function autosave($id)
{
if (!$this->request->isAJAX()) {
return $this->response->setJSON(['success' => false, 'message' => 'Invalid request']);
}
$page = $this->pageModel->find($id);
if (!$page) {
return $this->response->setJSON(['success' => false, 'message' => 'Page not found']);
}
$contentJson = $this->request->getPost('content_json') ?? '{}';
$contentHtml = $this->request->getPost('content_html') ?? '';
// Validate JSON
$blocks = [];
if (!empty($contentJson)) {
$parsed = json_decode($contentJson, true);
if (json_last_error() === JSON_ERROR_NONE && isset($parsed['blocks'])) {
$blocks = $parsed['blocks'];
}
}
// Render HTML if not provided
if (empty($contentHtml) && !empty($blocks)) {
$contentHtml = ContentRenderer::renderEditorJsToHtml($blocks);
}
// Sanitize HTML
$contentHtml = $this->sanitizeHtml($contentHtml);
// Extract excerpt
$excerpt = '';
if (!empty($blocks)) {
$excerpt = ContentRenderer::extractExcerpt($blocks);
}
$data = [
'content_json' => $contentJson,
'content_html' => $contentHtml,
'excerpt' => $excerpt,
];
try {
$this->pageModel->skipValidation(true);
$this->pageModel->update($id, $data);
return $this->response->setJSON([
'success' => true,
'message' => 'Autosaved',
'timestamp' => date('Y-m-d H:i:s'),
]);
} catch (\Exception $e) {
log_message('error', 'Autosave failed - ID: ' . $id . ', Error: ' . $e->getMessage());
return $this->response->setJSON(['success' => false, 'message' => 'Autosave failed']);
}
}
/**
* Upload image (AJAX)
*/
public function upload()
{
if (!$this->request->isAJAX()) {
return $this->response->setJSON(['success' => 0, 'message' => 'Invalid request']);
}
$file = $this->request->getFile('image');
if (!$file || !$file->isValid()) {
return $this->response->setJSON(['success' => 0, 'message' => 'No file uploaded']);
}
// Validate file type
$allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
if (!in_array($file->getMimeType(), $allowedTypes)) {
return $this->response->setJSON(['success' => 0, 'message' => 'Invalid file type. Only JPG, PNG, and WebP are allowed.']);
}
// Validate file size (2MB max)
if ($file->getSize() > 2 * 1024 * 1024) {
return $this->response->setJSON(['success' => 0, 'message' => 'File size exceeds 2MB limit.']);
}
// Generate random filename
$extension = $file->getExtension();
$newName = uniqid('page_', true) . '.' . $extension;
$uploadPath = WRITEPATH . 'uploads/pages/';
// Create directory if not exists
if (!is_dir($uploadPath)) {
mkdir($uploadPath, 0755, true);
}
// Move file
if ($file->move($uploadPath, $newName)) {
$url = base_url('writable/uploads/pages/' . $newName);
return $this->response->setJSON([
'success' => 1,
'file' => [
'url' => $url,
],
]);
}
return $this->response->setJSON(['success' => 0, 'message' => 'Upload failed']);
}
/**
* Delete page
*/
public function delete($id)
{
$page = $this->pageModel->find($id);
if (!$page) {
return redirect()->to('/admin/pages')
->with('error', 'Halaman tidak ditemukan.');
}
$userId = session()->get('user_id');
if ($this->pageModel->delete($id)) {
// Log action
$this->auditLogModel->logAction('page_deleted', $userId);
return redirect()->to('/admin/pages')
->with('success', 'Halaman berhasil dihapus.');
}
return redirect()->to('/admin/pages')
->with('error', 'Gagal menghapus halaman.');
}
/**
* Sanitize HTML using basic PHP functions
* For production, consider using HTMLPurifier library
*
* @param string $html
* @return string
*/
protected function sanitizeHtml(string $html): string
{
// Basic sanitization - allow common HTML tags
$allowedTags = '<p><h1><h2><h3><h4><h5><h6><ul><ol><li><blockquote><cite><pre><code><table><tbody><tr><td><th><hr><figure><img><figcaption><a><div><strong><em><u><s><br>';
// Strip all tags except allowed
$html = strip_tags($html, $allowedTags);
// Remove dangerous attributes
$html = preg_replace('/on\w+="[^"]*"/i', '', $html);
$html = preg_replace('/javascript:/i', '', $html);
return $html;
}
}

View File

@@ -0,0 +1,134 @@
<?php
namespace App\Controllers\Admin;
use App\Controllers\BaseController;
use App\Models\UserModel;
class Profile extends BaseController
{
protected $userModel;
public function __construct()
{
$this->userModel = new UserModel();
}
/**
* Display profile edit form
*/
public function index()
{
$userId = session()->get('user_id');
if (!$userId) {
return redirect()->to('/auth/login')
->with('error', 'Silakan login terlebih dahulu.');
}
$user = $this->userModel->find($userId);
if (!$user) {
return redirect()->to('/admin/dashboard')
->with('error', 'User tidak ditemukan.');
}
$data = [
'title' => 'Edit Profile',
'user' => $user,
];
return view('admin/profile/index', $data);
}
/**
* Update profile
*/
public function update()
{
$userId = session()->get('user_id');
if (!$userId) {
return redirect()->to('/auth/login')
->with('error', 'Silakan login terlebih dahulu.');
}
$user = $this->userModel->find($userId);
if (!$user) {
return redirect()->to('/admin/dashboard')
->with('error', 'User tidak ditemukan.');
}
$validation = \Config\Services::validation();
$rules = [
'username' => 'required|min_length[3]|max_length[100]',
'email' => 'required|valid_email|max_length[255]',
'phone_number' => 'permit_empty|max_length[20]',
'telegram_id' => 'permit_empty|integer',
];
if (!$this->validate($rules)) {
return redirect()->back()
->withInput()
->with('errors', $validation->getErrors());
}
// Check if username is unique (except current user)
$existingUser = $this->userModel->where('username', $this->request->getPost('username'))
->where('id !=', $userId)
->first();
if ($existingUser) {
return redirect()->back()
->withInput()
->with('errors', ['username' => 'Username sudah digunakan.']);
}
// Check if email is unique (except current user)
$existingEmail = $this->userModel->where('email', $this->request->getPost('email'))
->where('id !=', $userId)
->first();
if ($existingEmail) {
return redirect()->back()
->withInput()
->with('errors', ['email' => 'Email sudah digunakan.']);
}
$data = [
'username' => $this->request->getPost('username'),
'email' => $this->request->getPost('email'),
'phone_number' => $this->request->getPost('phone_number') ?: null,
'telegram_id' => $this->request->getPost('telegram_id') ?: null,
];
// Update password if provided
$newPassword = $this->request->getPost('password');
if (!empty($newPassword)) {
if (strlen($newPassword) < 6) {
return redirect()->back()
->withInput()
->with('error', 'Password minimal 6 karakter.');
}
$data['password_hash'] = password_hash($newPassword, PASSWORD_DEFAULT);
}
if ($this->userModel->update($userId, $data)) {
// Update session data
session()->set([
'username' => $data['username'],
'email' => $data['email'],
]);
return redirect()->to('/admin/profile')
->with('success', 'Profile berhasil diperbarui.');
}
return redirect()->back()
->withInput()
->with('error', 'Gagal memperbarui profile.');
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace App\Controllers\Admin;
use App\Controllers\BaseController;
use App\Models\SettingsModel;
class Settings extends BaseController
{
protected $settingsModel;
public function __construct()
{
$this->settingsModel = new SettingsModel();
// Check if user is admin
if (session()->get('role') !== 'admin') {
throw new \CodeIgniter\Exceptions\PageNotFoundException();
}
}
/**
* Display settings form
*/
public function index()
{
// Get all settings
$settings = $this->settingsModel->findAll();
// Convert to key-value array for easier access
$settingsArray = [];
foreach ($settings as $setting) {
$settingsArray[$setting['key']] = $setting;
}
$data = [
'title' => 'Pengaturan',
'settings' => $settingsArray,
];
return view('admin/settings/index', $data);
}
/**
* Update settings
*/
public function update()
{
$validation = \Config\Services::validation();
$rules = [
'site_name' => 'required|min_length[3]|max_length[100]',
'site_description' => 'permit_empty|max_length[255]',
];
if (!$this->validate($rules)) {
return redirect()->back()
->withInput()
->with('errors', $validation->getErrors());
}
$siteName = $this->request->getPost('site_name');
$siteDescription = $this->request->getPost('site_description') ?: '';
// Update or create site_name
$this->settingsModel->setSetting(
'site_name',
$siteName,
'Nama situs yang ditampilkan di sidebar dan judul halaman'
);
// Update or create site_description
$this->settingsModel->setSetting(
'site_description',
$siteDescription,
'Deskripsi singkat tentang situs'
);
return redirect()->to('/admin/settings')
->with('success', 'Pengaturan berhasil diperbarui.');
}
}

View File

@@ -0,0 +1,356 @@
<?php
namespace App\Controllers\Admin;
use App\Controllers\BaseController;
use App\Models\UserModel;
use App\Models\RoleModel;
use App\Models\AuditLogModel;
class Users extends BaseController
{
protected $userModel;
protected $roleModel;
protected $auditLogModel;
public function __construct()
{
$this->userModel = new UserModel();
$this->roleModel = new RoleModel();
$this->auditLogModel = new AuditLogModel();
// Check if user is admin
if (session()->get('role') !== 'admin') {
throw new \CodeIgniter\Exceptions\PageNotFoundException();
}
}
/**
* Display list of users
*/
public function index()
{
$perPage = 10;
$page = $this->request->getGet('page') ?? 1;
$role = $this->request->getGet('role');
$status = $this->request->getGet('status');
$search = $this->request->getGet('search');
// Build query with filters
$this->userModel->select('users.*, roles.name as role_name')
->join('roles', 'roles.id = users.role_id', 'left');
// Filter by role
if ($role) {
$this->userModel->where('roles.name', $role);
}
// Filter by status
if ($status !== null && $status !== '') {
$this->userModel->where('users.is_active', $status);
}
// Search
if ($search) {
$this->userModel->groupStart()
->like('users.username', $search)
->orLike('users.email', $search)
->orLike('users.phone_number', $search)
->groupEnd();
}
// Get paginated results
$users = $this->userModel->orderBy('users.created_at', 'DESC')
->paginate($perPage, 'default', $page);
$pager = $this->userModel->pager;
// Get roles for filter
$roles = $this->roleModel->findAll();
$data = [
'title' => 'Pengguna',
'users' => $users,
'pager' => $pager,
'roles' => $roles,
'currentRole' => $role,
'currentStatus' => $status,
'currentSearch' => $search,
'stats' => [
'total' => $this->userModel->countAllResults(),
'active' => $this->userModel->where('is_active', 1)->countAllResults(),
'inactive' => $this->userModel->where('is_active', 0)->countAllResults(),
],
];
return view('admin/users/index', $data);
}
/**
* Show form to create new user
*/
public function create()
{
$roles = $this->roleModel->findAll();
$data = [
'title' => 'Tambah Pengguna',
'user' => null,
'roles' => $roles,
];
return view('admin/users/form', $data);
}
/**
* Store new user
*/
public function store()
{
$validation = \Config\Services::validation();
$rules = [
'username' => 'required|min_length[3]|max_length[100]|is_unique[users.username]',
'email' => 'required|valid_email|max_length[255]|is_unique[users.email]',
'password' => 'required|min_length[6]',
'role_id' => 'required|integer',
'phone_number' => 'permit_empty|max_length[20]|is_unique[users.phone_number]',
'telegram_id' => 'permit_empty|integer|is_unique[users.telegram_id]',
];
if (!$this->validate($rules)) {
return redirect()->back()
->withInput()
->with('validation', $validation);
}
$username = $this->request->getPost('username');
$email = $this->request->getPost('email');
$password = $this->request->getPost('password');
$roleId = $this->request->getPost('role_id');
$phoneNumber = $this->request->getPost('phone_number');
$telegramId = $this->request->getPost('telegram_id');
$isActive = $this->request->getPost('is_active') ? 1 : 0;
$userId = session()->get('user_id');
$data = [
'username' => $username,
'email' => $email,
'password_hash' => $password, // Will be hashed by beforeInsert
'role_id' => $roleId,
'phone_number' => !empty($phoneNumber) ? $phoneNumber : null,
'telegram_id' => !empty($telegramId) ? $telegramId : null,
'is_active' => $isActive,
];
if ($this->userModel->insert($data)) {
// Log action
$this->auditLogModel->logAction('user_created', $userId);
return redirect()->to('/admin/users')
->with('success', 'Pengguna berhasil ditambahkan.');
}
return redirect()->back()
->withInput()
->with('error', 'Gagal menambahkan pengguna.');
}
/**
* Show form to edit user
*/
public function edit($id)
{
$user = $this->userModel->find($id);
if (!$user) {
return redirect()->to('/admin/users')
->with('error', 'Pengguna tidak ditemukan.');
}
$roles = $this->roleModel->findAll();
$data = [
'title' => 'Edit Pengguna',
'user' => $user,
'roles' => $roles,
];
return view('admin/users/form', $data);
}
/**
* Update user
*/
public function update($id)
{
$user = $this->userModel->find($id);
if (!$user) {
return redirect()->to('/admin/users')
->with('error', 'Pengguna tidak ditemukan.');
}
$validation = \Config\Services::validation();
$rules = [
'username' => "required|min_length[3]|max_length[100]|is_unique[users.username,id,{$id}]",
'email' => "required|valid_email|max_length[255]|is_unique[users.email,id,{$id}]",
'role_id' => 'required|integer',
'phone_number' => "permit_empty|max_length[20]|is_unique[users.phone_number,id,{$id}]",
'telegram_id' => "permit_empty|integer|is_unique[users.telegram_id,id,{$id}]",
];
if (!$this->validate($rules)) {
return redirect()->back()
->withInput()
->with('validation', $validation);
}
$username = $this->request->getPost('username');
$email = $this->request->getPost('email');
$roleId = $this->request->getPost('role_id');
$phoneNumber = $this->request->getPost('phone_number');
$telegramId = $this->request->getPost('telegram_id');
$isActive = $this->request->getPost('is_active') ? 1 : 0;
$userId = session()->get('user_id');
$data = [
'username' => $username,
'email' => $email,
'role_id' => $roleId,
'phone_number' => !empty($phoneNumber) ? $phoneNumber : null,
'telegram_id' => !empty($telegramId) ? $telegramId : null,
'is_active' => $isActive,
];
if ($this->userModel->update($id, $data)) {
// Log action
$this->auditLogModel->logAction('user_updated', $userId);
return redirect()->to('/admin/users')
->with('success', 'Pengguna berhasil diperbarui.');
}
return redirect()->back()
->withInput()
->with('error', 'Gagal memperbarui pengguna.');
}
/**
* Reset user password
*/
public function resetPassword($id)
{
$user = $this->userModel->find($id);
if (!$user) {
return redirect()->to('/admin/users')
->with('error', 'Pengguna tidak ditemukan.');
}
$validation = \Config\Services::validation();
$rules = [
'new_password' => 'required|min_length[6]',
'confirm_password' => 'required|matches[new_password]',
];
if (!$this->validate($rules)) {
return redirect()->back()
->withInput()
->with('validation', $validation)
->with('error', 'Password tidak valid atau tidak cocok.');
}
$newPassword = $this->request->getPost('new_password');
$userId = session()->get('user_id');
$data = [
'password_hash' => $newPassword, // Will be hashed by beforeUpdate
];
if ($this->userModel->update($id, $data)) {
// Log action
$this->auditLogModel->logAction('user_password_reset', $userId);
return redirect()->to('/admin/users')
->with('success', 'Password pengguna berhasil direset.');
}
return redirect()->back()
->with('error', 'Gagal mereset password.');
}
/**
* Toggle user active status
*/
public function toggleActive($id)
{
$user = $this->userModel->find($id);
if (!$user) {
return redirect()->to('/admin/users')
->with('error', 'Pengguna tidak ditemukan.');
}
// Prevent deactivating yourself
if ($id == session()->get('user_id')) {
return redirect()->to('/admin/users')
->with('error', 'Anda tidak dapat menonaktifkan akun sendiri.');
}
$newStatus = $user['is_active'] ? 0 : 1;
$userId = session()->get('user_id');
$data = [
'is_active' => $newStatus,
];
if ($this->userModel->update($id, $data)) {
// Log action
$action = $newStatus ? 'user_activated' : 'user_deactivated';
$this->auditLogModel->logAction($action, $userId);
$message = $newStatus ? 'Pengguna berhasil diaktifkan.' : 'Pengguna berhasil dinonaktifkan.';
return redirect()->to('/admin/users')
->with('success', $message);
}
return redirect()->to('/admin/users')
->with('error', 'Gagal mengubah status pengguna.');
}
/**
* Delete user
*/
public function delete($id)
{
$user = $this->userModel->find($id);
if (!$user) {
return redirect()->to('/admin/users')
->with('error', 'Pengguna tidak ditemukan.');
}
// Prevent deleting yourself
if ($id == session()->get('user_id')) {
return redirect()->to('/admin/users')
->with('error', 'Anda tidak dapat menghapus akun sendiri.');
}
$userId = session()->get('user_id');
if ($this->userModel->delete($id)) {
// Log action
$this->auditLogModel->logAction('user_deleted', $userId);
return redirect()->to('/admin/users')
->with('success', 'Pengguna berhasil dihapus.');
}
return redirect()->to('/admin/users')
->with('error', 'Gagal menghapus pengguna.');
}
}

View File

@@ -0,0 +1,382 @@
<?php
namespace App\Controllers;
use App\Models\UserModel;
use App\Models\RoleModel;
use App\Models\AuditLogModel;
use App\Models\LoginAttemptModel;
use CodeIgniter\HTTP\RedirectResponse;
class AuthController extends BaseController
{
protected $userModel;
protected $roleModel;
protected $auditLogModel;
protected $loginAttemptModel;
protected $throttler;
/**
* Konfigurasi rate limiting - BERDASARKAN FAILED ATTEMPTS SAJA
* Environment-aware: lebih longgar di development, ketat di production
*/
protected function getRateLimitConfig(): array
{
if (ENVIRONMENT === 'production') {
return [
'soft_limit' => 5, // Delay setelah 5 failed attempts
'hard_limit' => 20, // Block (429) setelah 20 failed attempts
'ttl_seconds' => 900, // 15 menit
'delay_ms' => 500, // Delay 500ms setelah soft_limit
];
} else {
// Development: lebih longgar untuk testing
return [
'soft_limit' => 20, // Delay setelah 20 failed attempts
'hard_limit' => 100, // Block setelah 100 failed attempts
'ttl_seconds' => 900, // 15 menit
'delay_ms' => 200, // Delay 200ms setelah soft_limit
];
}
}
public function __construct()
{
$this->userModel = new UserModel();
$this->roleModel = new RoleModel();
$this->auditLogModel = new AuditLogModel();
$this->loginAttemptModel = new LoginAttemptModel();
$this->throttler = \Config\Services::throttler();
}
public function login()
{
// If already logged in, redirect to admin dashboard
if (session()->get('is_logged_in')) {
return redirect()->to('/admin');
}
// Debug: Log request method
$method = $this->request->getMethod();
log_message('debug', 'Login method: ' . $method);
log_message('debug', 'Request URI: ' . $this->request->getUri()->getPath());
log_message('debug', 'Is POST? ' . ($method === 'post' ? 'YES' : 'NO'));
if (strtolower($method) === 'post') {
try {
// ============================================================
// INITIALIZE RATE LIMITING COUNTERS
// ============================================================
$ipAddress = $this->request->getIPAddress();
$cfg = $this->getRateLimitConfig();
$cache = \Config\Services::cache();
// Normalize username - handle berbagai format input (audit tool variations)
$usernameRaw = $this->request->getPost('username')
?? $this->request->getPost('email')
?? $this->request->getPost('identity')
?? '';
$usernameNormalized = strtolower(trim($usernameRaw));
if (empty($usernameNormalized)) {
$usernameNormalized = 'unknown';
}
// Dual-key counter: per IP+username dan per IP (untuk handle random usernames)
// Dual-key counter: per IP+username dan per IP (untuk handle random usernames dari audit tool)
// Gunakan underscore bukan colon untuk menghindari reserved characters {}()/\@:
$keyUser = 'login_fail_' . md5($ipAddress . '_' . $usernameNormalized);
$keyIp = 'login_fail_ip_' . md5($ipAddress);
$failUser = $cache->get($keyUser) ?? 0;
$failIp = $cache->get($keyIp) ?? 0;
$failMax = max($failUser, $failIp);
// HARD LIMIT CHECK - Block sebelum validasi (audit must see 429)
if ($failMax >= $cfg['hard_limit']) {
$this->loginAttemptModel->recordAttempt($ipAddress, $usernameRaw, null, false);
log_message('warning', "Hard rate limit exceeded - Fail count: {$failMax} (User: {$failUser}, IP: {$failIp}) - IP: {$ipAddress}");
$response = service('response');
$response->setStatusCode(429);
$response->setHeader('Retry-After', (string) $cfg['ttl_seconds']);
$response->setHeader('X-RateLimit-Limit', (string) $cfg['hard_limit']);
$response->setHeader('X-RateLimit-Remaining', '0');
$response->setHeader('Content-Type', 'text/html; charset=utf-8');
return view('auth/login', [
'error' => 'Terlalu banyak percobaan login. Silakan coba lagi dalam ' . ($cfg['ttl_seconds'] / 60) . ' menit.',
]);
}
// ============================================================
// VALIDASI INPUT
// ============================================================
$password = $this->request->getPost('password') ?? '';
$validation = \Config\Services::validation();
$rules = [
'username' => 'required|min_length[3]|max_length[100]',
'password' => 'required|min_length[6]',
];
// Validation error = failed attempt
if (!$this->validate($rules)) {
$this->incrementFailedAttempts($cache, $keyUser, $keyIp, $cfg);
$this->loginAttemptModel->recordAttempt($ipAddress, $usernameRaw, null, false);
return view('auth/login', [
'validation' => $validation,
]);
}
if (empty($usernameRaw) || empty($password)) {
$this->incrementFailedAttempts($cache, $keyUser, $keyIp, $cfg);
$this->loginAttemptModel->recordAttempt($ipAddress, $usernameRaw, null, false);
return view('auth/login', [
'error' => 'Username dan password harus diisi.',
]);
}
// ============================================================
// CEK SOFT LIMIT SEBELUM VALIDASI PASSWORD
// ============================================================
// Jika sudah mencapai soft_limit, block SEBELUM validasi password
// Ini mencegah user dengan password benar tetap bisa login setelah banyak failed attempts
if ($failMax >= $cfg['soft_limit']) {
log_message('warning', "Soft rate limit exceeded BEFORE password check - Fail count: {$failMax} (User: {$failUser}, IP: {$failIp}) - IP: {$ipAddress}, Username: {$usernameRaw}");
$response = service('response');
$response->setStatusCode(429);
$response->setHeader('Retry-After', (string) $cfg['ttl_seconds']);
$response->setHeader('X-RateLimit-Limit', (string) $cfg['soft_limit']);
$response->setHeader('X-RateLimit-Remaining', '0');
$response->setHeader('Content-Type', 'text/html; charset=utf-8');
return view('auth/login', [
'error' => 'Terlalu banyak percobaan login yang gagal. Silakan coba lagi dalam ' . ($cfg['ttl_seconds'] / 60) . ' menit.',
]);
}
// ============================================================
// VERIFIKASI USER DAN PASSWORD
// ============================================================
$user = $this->userModel->getUserByUsername($usernameRaw);
$passwordValid = false;
if ($user) {
$passwordValid = $this->userModel->verifyPassword($password, $user['password_hash']);
}
// User not found atau password salah = failed attempt
if (!$user || !$passwordValid) {
$this->incrementFailedAttempts($cache, $keyUser, $keyIp, $cfg);
$this->loginAttemptModel->recordAttempt($ipAddress, $usernameRaw, $user['id'] ?? null, false);
// Get updated fail count setelah increment
$failUser = $cache->get($keyUser) ?? 0;
$failIp = $cache->get($keyIp) ?? 0;
$failMax = max($failUser, $failIp);
// Cek lagi setelah increment - jika sudah mencapai soft_limit, block
if ($failMax >= $cfg['soft_limit']) {
log_message('warning', "Soft rate limit exceeded AFTER increment - Fail count: {$failMax} (User: {$failUser}, IP: {$failIp}) - IP: {$ipAddress}, Username: {$usernameRaw}");
$response = service('response');
$response->setStatusCode(429);
$response->setHeader('Retry-After', (string) $cfg['ttl_seconds']);
$response->setHeader('X-RateLimit-Limit', (string) $cfg['soft_limit']);
$response->setHeader('X-RateLimit-Remaining', '0');
$response->setHeader('Content-Type', 'text/html; charset=utf-8');
return view('auth/login', [
'error' => 'Terlalu banyak percobaan login yang gagal. Silakan coba lagi dalam ' . ($cfg['ttl_seconds'] / 60) . ' menit.',
]);
}
log_message('info', "Login failed - IP: {$ipAddress}, Username: {$usernameRaw}, Fail count: {$failMax}");
return view('auth/login', [
'error' => 'Username atau password salah.',
]);
}
// Check if user is active
if (!$user['is_active']) {
$this->incrementFailedAttempts($cache, $keyUser, $keyIp, $cfg);
$this->loginAttemptModel->recordAttempt($ipAddress, $usernameRaw, $user['id'], false);
// Apply soft limit delay
$failUser = $cache->get($keyUser) ?? 0;
$failIp = $cache->get($keyIp) ?? 0;
$failMax = max($failUser, $failIp);
if ($failMax >= $cfg['soft_limit']) {
usleep($cfg['delay_ms'] * 1000);
}
return view('auth/login', [
'error' => 'Akun Anda telah dinonaktifkan.',
]);
}
// ============================================================
// VERIFIKASI ROLE
// ============================================================
$role = $this->roleModel->find($user['role_id']);
$roleName = $role ? $role['name'] : 'editor';
// Check if role is admin or editor
if (!in_array($roleName, ['admin', 'editor'])) {
$this->incrementFailedAttempts($cache, $keyUser, $keyIp, $cfg);
$this->loginAttemptModel->recordAttempt($ipAddress, $usernameRaw, $user['id'], false);
// Apply soft limit delay
$failUser = $cache->get($keyUser) ?? 0;
$failIp = $cache->get($keyIp) ?? 0;
$failMax = max($failUser, $failIp);
if ($failMax >= $cfg['soft_limit']) {
usleep($cfg['delay_ms'] * 1000);
}
return view('auth/login', [
'error' => 'Anda tidak memiliki akses ke sistem ini.',
]);
}
// ============================================================
// SESSION MANAGEMENT - Mencegah Session Fixation Attack
// ============================================================
// URUTAN PENTING: Set session data DULU, baru regenerate
// Ini memastikan session ID berubah setelah privilege escalation
$session = session();
// Set session data TERLEBIH DAHULU
$session->set([
'is_logged_in' => true,
'user_id' => $user['id'],
'username' => $user['username'],
'email' => $user['email'],
'role' => $roleName,
'role_id' => $user['role_id'],
]);
// Dapatkan session ID sebelum regenerate (untuk logging)
$oldSessionId = session_id();
// Regenerate session ID SETELAH set session data
// Parameter true = destroy old session data untuk keamanan maksimal
$session->regenerate(true);
// Dapatkan session ID baru setelah regenerate
$newSessionId = session_id();
log_message('info', "Session regenerated after login - Old: {$oldSessionId}, New: {$newSessionId}");
// Verifikasi session ID benar-benar berubah
if ($oldSessionId === $newSessionId) {
log_message('warning', "Session ID tidak berubah setelah regenerate! Memaksa regenerate lagi...");
$session->regenerate(true);
$newSessionId = session_id();
log_message('info', "Session ID setelah regenerate kedua: {$newSessionId}");
}
// ============================================================
// RECORD SUCCESSFUL LOGIN & RESET FAILED ATTEMPTS
// ============================================================
// Record successful login attempt
$this->loginAttemptModel->recordAttempt($ipAddress, $usernameRaw, $user['id'], true);
// Reset failed attempts counter karena login berhasil
// Password benar = reset fail count (boleh bypass soft limit)
$cache->delete($keyUser);
$cache->delete($keyIp);
log_message('info', "Login successful - Failed attempts counter reset for IP: {$ipAddress}, Username: {$usernameRaw}");
// Update last login
$this->userModel->update($user['id'], [
'last_login_at' => date('Y-m-d H:i:s'),
]);
// Log login action ke audit log
$this->auditLogModel->logAction('login', $user['id']);
log_message('info', "Login successful - User: {$user['username']} (ID: {$user['id']}) from IP: {$ipAddress}");
// Optional: Send Telegram notification if telegram_id exists
if (!empty($user['telegram_id'])) {
$this->sendTelegramNotification($user['telegram_id'], $user['username']);
}
return redirect()->to('/admin')->with('success', 'Selamat datang, ' . $user['username'] . '!');
} catch (\Exception $e) {
log_message('error', 'Login error: ' . $e->getMessage() . ' | Trace: ' . $e->getTraceAsString());
return view('auth/login', [
'error' => 'Terjadi kesalahan saat login: ' . $e->getMessage(),
]);
}
}
return view('auth/login');
}
public function logout(): RedirectResponse
{
$userId = session()->get('user_id');
// Log logout action before destroying session
if ($userId) {
try {
$this->auditLogModel->logAction('logout', $userId);
} catch (\Exception $e) {
log_message('error', 'Logout audit log failed: ' . $e->getMessage());
// Continue with logout even if audit log fails
}
}
// Destroy session
session()->destroy();
return redirect()->to('/auth/login')->with('success', 'Anda telah berhasil logout.');
}
/**
* Increment failed attempts counter (dual-key: IP+username dan IP)
*
* @param \CodeIgniter\Cache\CacheInterface $cache
* @param string $keyUser Cache key untuk IP+username
* @param string $keyIp Cache key untuk IP
* @param array $cfg Rate limit configuration
*/
protected function incrementFailedAttempts($cache, string $keyUser, string $keyIp, array $cfg): void
{
$failUser = $cache->get($keyUser) ?? 0;
$failIp = $cache->get($keyIp) ?? 0;
$failUser++;
$failIp++;
$cache->save($keyUser, $failUser, $cfg['ttl_seconds']);
$cache->save($keyIp, $failIp, $cfg['ttl_seconds']);
}
/**
* Optional: Send Telegram notification on login
*/
protected function sendTelegramNotification($telegramId, $username)
{
// This is optional - implement if you have Telegram bot configured
// Example implementation:
// $botToken = getenv('TELEGRAM_BOT_TOKEN');
// if ($botToken) {
// $message = "Login berhasil untuk user: {$username}";
// $url = "https://api.telegram.org/bot{$botToken}/sendMessage";
// $data = ['chat_id' => $telegramId, 'text' => $message];
// // Use HTTP client to send request
// }
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Controllers;
use CodeIgniter\Controller;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Psr\Log\LoggerInterface;
/**
* BaseController provides a convenient place for loading components
* and performing functions that are needed by all your controllers.
*
* Extend this class in any new controllers:
* ```
* class Home extends BaseController
* ```
*
* For security, be sure to declare any new methods as protected or private.
*/
abstract class BaseController extends Controller
{
/**
* Be sure to declare properties for any property fetch you initialized.
* The creation of dynamic property is deprecated in PHP 8.2.
*/
// protected $session;
/**
* @return void
*/
public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger)
{
// Load here all helpers you want to be available in your controllers that extend BaseController.
// Caution: Do not put the this below the parent::initController() call below.
// $this->helpers = ['form', 'url'];
// Caution: Do not edit this line.
parent::initController($request, $response, $logger);
// Preload any models, libraries, etc, here.
// $this->session = service('session');
}
}

11
app/Controllers/Home.php Normal file
View File

@@ -0,0 +1,11 @@
<?php
namespace App\Controllers;
class Home extends BaseController
{
public function index(): string
{
return view('welcome_message');
}
}