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

0
app/Models/.gitkeep Normal file
View File

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Models;
use CodeIgniter\Model;
class AuditLogModel extends Model
{
protected $table = 'audit_logs';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $protectFields = true;
protected $allowedFields = ['user_id', 'action', 'ip_address', 'user_agent', 'created_at'];
protected bool $allowEmptyInserts = false;
protected $useTimestamps = false;
protected $dateFormat = 'datetime';
protected $createdField = 'created_at';
protected $updatedField = null;
protected $deletedField = null;
public function logAction(string $action, ?int $userId = null): bool
{
$request = service('request');
$data = [
'user_id' => $userId,
'action' => $action,
'ip_address' => $request->getIPAddress(),
'user_agent' => $request->getUserAgent()->getAgentString(),
'created_at' => date('Y-m-d H:i:s'),
];
try {
return $this->insert($data);
} catch (\Exception $e) {
log_message('error', 'Audit log insert failed: ' . $e->getMessage());
return false;
}
}
}

View File

@@ -0,0 +1,151 @@
<?php
namespace App\Models;
use CodeIgniter\Model;
/**
* Model untuk Login Attempts
*
* Digunakan untuk:
* - Mencatat semua percobaan login
* - Menghitung jumlah percobaan gagal
* - Implementasi account lockout
* - Monitoring dan audit trail
*/
class LoginAttemptModel extends Model
{
protected $table = 'login_attempts';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $protectFields = true;
protected $allowedFields = ['ip_address', 'username', 'user_id', 'success', 'user_agent', 'created_at'];
protected bool $allowEmptyInserts = false;
protected $useTimestamps = true;
protected $dateFormat = 'datetime';
protected $createdField = 'created_at';
protected $updatedField = null;
protected $deletedField = null;
protected $validationRules = [];
protected $validationMessages = [];
protected $skipValidation = true; // Skip validation karena data dari sistem
protected $beforeInsert = [];
protected $afterInsert = [];
protected $beforeUpdate = [];
protected $afterUpdate = [];
protected $beforeFind = [];
protected $afterFind = [];
protected $beforeDelete = [];
protected $afterDelete = [];
/**
* Mencatat percobaan login
*
* @param string $ipAddress IP address dari request
* @param string|null $username Username yang dicoba
* @param int|null $userId ID user jika login berhasil
* @param bool $success Status login (true = berhasil, false = gagal)
* @return bool True jika berhasil disimpan
*/
public function recordAttempt(string $ipAddress, ?string $username = null, ?int $userId = null, bool $success = false): bool
{
$request = service('request');
$data = [
'ip_address' => $ipAddress,
'username' => $username,
'user_id' => $userId,
'success' => $success ? 1 : 0,
'user_agent' => $request->getUserAgent()->getAgentString(),
];
try {
return $this->insert($data) !== false;
} catch (\Exception $e) {
log_message('error', 'Failed to record login attempt: ' . $e->getMessage());
return false;
}
}
/**
* Menghitung jumlah percobaan gagal dalam periode tertentu
*
* @param string $ipAddress IP address
* @param int $minutes Periode waktu dalam menit (default: 15)
* @return int Jumlah percobaan gagal
*/
public function countFailedAttempts(string $ipAddress, int $minutes = 15): int
{
$timeLimit = date('Y-m-d H:i:s', strtotime("-{$minutes} minutes"));
return $this->where('ip_address', $ipAddress)
->where('success', 0)
->where('created_at >=', $timeLimit)
->countAllResults(false);
}
/**
* Menghitung jumlah percobaan gagal untuk username tertentu
*
* @param string $username Username
* @param int $minutes Periode waktu dalam menit (default: 15)
* @return int Jumlah percobaan gagal
*/
public function countFailedAttemptsByUsername(string $username, int $minutes = 15): int
{
$timeLimit = date('Y-m-d H:i:s', strtotime("-{$minutes} minutes"));
return $this->where('username', $username)
->where('success', 0)
->where('created_at >=', $timeLimit)
->countAllResults(false);
}
/**
* Menghapus record percobaan lama (cleanup)
*
* @param int $days Jumlah hari untuk menyimpan data (default: 30)
* @return int Jumlah record yang dihapus
*/
public function cleanupOldAttempts(int $days = 30): int
{
$timeLimit = date('Y-m-d H:i:s', strtotime("-{$days} days"));
return $this->where('created_at <', $timeLimit)->delete();
}
/**
* Mendapatkan semua percobaan login untuk IP tertentu
*
* @param string $ipAddress IP address
* @param int $limit Jumlah record yang diambil
* @return array Array of login attempts
*/
public function getAttemptsByIp(string $ipAddress, int $limit = 50): array
{
return $this->where('ip_address', $ipAddress)
->orderBy('created_at', 'DESC')
->findAll($limit);
}
/**
* Mendapatkan semua percobaan login untuk username tertentu
*
* @param string $username Username
* @param int $limit Jumlah record yang diambil
* @return array Array of login attempts
*/
public function getAttemptsByUsername(string $username, int $limit = 50): array
{
return $this->where('username', $username)
->orderBy('created_at', 'DESC')
->findAll($limit);
}
}

125
app/Models/NewsModel.php Normal file
View File

@@ -0,0 +1,125 @@
<?php
namespace App\Models;
use CodeIgniter\Model;
class NewsModel extends Model
{
protected $table = 'news';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $protectFields = true;
protected $allowedFields = ['title', 'slug', 'content', 'content_json', 'content_html', 'excerpt', 'status', 'published_at', 'created_by'];
protected bool $allowEmptyInserts = false;
protected $useTimestamps = true;
protected $dateFormat = 'datetime';
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $deletedField = null;
// Validation
protected $validationRules = [
'title' => 'required|min_length[3]|max_length[255]',
'slug' => 'required|max_length[255]|is_unique[news.slug,id,{id}]',
'content' => 'required',
'status' => 'required|in_list[draft,published]',
];
protected $validationMessages = [
'title' => [
'required' => 'Judul berita harus diisi.',
'min_length' => 'Judul berita minimal 3 karakter.',
'max_length' => 'Judul berita maksimal 255 karakter.',
],
'slug' => [
'required' => 'Slug harus diisi.',
'is_unique' => 'Slug sudah digunakan, silakan gunakan judul yang berbeda.',
],
'content' => [
'required' => 'Konten berita harus diisi.',
],
'status' => [
'required' => 'Status harus dipilih.',
'in_list' => 'Status harus draft atau published.',
],
];
protected $skipValidation = false;
protected $cleanValidationRules = true;
/**
* Generate slug from title
*/
public function generateSlug(string $title, ?int $excludeId = null): string
{
// Convert to lowercase and replace spaces with hyphens
$slug = strtolower(trim($title));
$slug = preg_replace('/[^a-z0-9-]/', '-', $slug);
$slug = preg_replace('/-+/', '-', $slug);
$slug = trim($slug, '-');
// If slug is empty, use timestamp
if (empty($slug)) {
$slug = 'news-' . time();
}
// Check if slug exists
$baseSlug = $slug;
$counter = 1;
while ($this->slugExists($slug, $excludeId)) {
$slug = $baseSlug . '-' . $counter;
$counter++;
}
return $slug;
}
/**
* Check if slug exists
*/
protected function slugExists(string $slug, ?int $excludeId = null): bool
{
$builder = $this->where('slug', $slug);
if ($excludeId !== null) {
$builder->where('id !=', $excludeId);
}
return $builder->countAllResults() > 0;
}
/**
* Get news with creator information
*/
public function getNewsWithCreator(int $limit = 10, int $offset = 0, ?string $status = null)
{
$builder = $this->select('news.*, users.username as creator_name')
->join('users', 'users.id = news.created_by', 'left');
if ($status !== null) {
$builder->where('news.status', $status);
}
return $builder->orderBy('news.created_at', 'DESC')
->limit($limit, $offset)
->findAll();
}
/**
* Count news by status
*/
public function countByStatus(?string $status = null): int
{
if ($status !== null) {
return $this->where('status', $status)->countAllResults();
}
return $this->countAllResults();
}
}

109
app/Models/PageModel.php Normal file
View File

@@ -0,0 +1,109 @@
<?php
namespace App\Models;
use CodeIgniter\Model;
class PageModel extends Model
{
protected $table = 'pages';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $protectFields = true;
protected $allowedFields = ['title', 'slug', 'content', 'content_json', 'content_html', 'excerpt', 'featured_image', 'status'];
protected bool $allowEmptyInserts = false;
protected $useTimestamps = true;
protected $dateFormat = 'datetime';
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $deletedField = null;
// Validation
protected $validationRules = [
'title' => 'required|min_length[3]|max_length[255]',
'slug' => 'permit_empty|max_length[255]|is_unique[pages.slug,id,{id}]',
'content_json' => 'permit_empty',
'content_html' => 'permit_empty',
'status' => 'required|in_list[draft,published]',
];
protected $validationMessages = [
'title' => [
'required' => 'Judul halaman harus diisi.',
'min_length' => 'Judul halaman minimal 3 karakter.',
'max_length' => 'Judul halaman maksimal 255 karakter.',
],
'slug' => [
'required' => 'Slug harus diisi.',
'is_unique' => 'Slug sudah digunakan, silakan gunakan judul yang berbeda.',
],
'content' => [
'required' => 'Konten halaman harus diisi.',
],
'status' => [
'required' => 'Status harus dipilih.',
'in_list' => 'Status harus draft atau published.',
],
];
protected $skipValidation = false;
protected $cleanValidationRules = true;
/**
* Generate slug from title
*/
public function generateSlug(string $title, ?int $excludeId = null): string
{
// Convert to lowercase and replace spaces with hyphens
$slug = strtolower(trim($title));
$slug = preg_replace('/[^a-z0-9-]/', '-', $slug);
$slug = preg_replace('/-+/', '-', $slug);
$slug = trim($slug, '-');
// If slug is empty, use timestamp
if (empty($slug)) {
$slug = 'page-' . time();
}
// Check if slug exists
$baseSlug = $slug;
$counter = 1;
while ($this->slugExists($slug, $excludeId)) {
$slug = $baseSlug . '-' . $counter;
$counter++;
}
return $slug;
}
/**
* Check if slug exists
*/
protected function slugExists(string $slug, ?int $excludeId = null): bool
{
$builder = $this->where('slug', $slug);
if ($excludeId !== null) {
$builder->where('id !=', $excludeId);
}
return $builder->countAllResults() > 0;
}
/**
* Count pages by status
*/
public function countByStatus(?string $status = null): int
{
if ($status !== null) {
return $this->where('status', $status)->countAllResults();
}
return $this->countAllResults();
}
}

29
app/Models/RoleModel.php Normal file
View File

@@ -0,0 +1,29 @@
<?php
namespace App\Models;
use CodeIgniter\Model;
class RoleModel extends Model
{
protected $table = 'roles';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $protectFields = true;
protected $allowedFields = ['name'];
protected bool $allowEmptyInserts = false;
protected $useTimestamps = false;
protected $validationRules = [
'name' => 'required|max_length[50]|is_unique[roles.name,id,{id}]',
];
protected $validationMessages = [];
protected $skipValidation = false;
protected $cleanValidationRules = true;
}

View File

@@ -0,0 +1,68 @@
<?php
namespace App\Models;
use CodeIgniter\Model;
class SettingsModel extends Model
{
protected $table = 'settings';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $protectFields = true;
protected $allowedFields = ['key', 'value', 'description'];
protected bool $allowEmptyInserts = false;
protected $useTimestamps = true;
protected $dateFormat = 'datetime';
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $deletedField = null;
/**
* Get setting value by key
*/
public function getSetting(string $key, ?string $default = null): ?string
{
$setting = $this->where('key', $key)->first();
return $setting ? $setting['value'] : $default;
}
/**
* Set setting value by key
*/
public function setSetting(string $key, ?string $value, ?string $description = null): bool
{
$setting = $this->where('key', $key)->first();
if ($setting) {
return $this->update($setting['id'], [
'value' => $value,
'description' => $description ?? $setting['description'],
]);
} else {
return $this->insert([
'key' => $key,
'value' => $value,
'description' => $description,
]);
}
}
/**
* Get all settings as key-value array
*/
public function getAllSettings(): array
{
$settings = $this->findAll();
$result = [];
foreach ($settings as $setting) {
$result[$setting['key']] = $setting['value'];
}
return $result;
}
}

66
app/Models/UserModel.php Normal file
View File

@@ -0,0 +1,66 @@
<?php
namespace App\Models;
use CodeIgniter\Model;
class UserModel extends Model
{
protected $table = 'users';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $protectFields = true;
protected $allowedFields = ['role_id', 'username', 'email', 'phone_number', 'password_hash', 'telegram_id', 'is_active', 'last_login_at'];
protected bool $allowEmptyInserts = false;
protected $useTimestamps = true;
protected $dateFormat = 'datetime';
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $deletedField = null;
protected $validationRules = [
'username' => 'required|max_length[100]|is_unique[users.username,id,{id}]',
'email' => 'required|valid_email|max_length[255]|is_unique[users.email,id,{id}]',
'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}]',
'role_id' => 'required|integer',
];
protected $validationMessages = [];
protected $skipValidation = false;
protected $cleanValidationRules = true;
protected $beforeInsert = ['hashPassword'];
protected $beforeUpdate = ['hashPassword'];
protected function hashPassword(array $data)
{
if (isset($data['data']['password_hash']) && !empty($data['data']['password_hash'])) {
// Only hash if it's not already hashed (check if it starts with $2y$ which is bcrypt)
if (!preg_match('/^\$2[ayb]\$.{56}$/', $data['data']['password_hash'])) {
$data['data']['password_hash'] = password_hash($data['data']['password_hash'], PASSWORD_DEFAULT);
}
}
return $data;
}
public function getUserByUsername(string $username)
{
return $this->where('username', $username)->first();
}
public function getUserByEmail(string $email)
{
return $this->where('email', $email)->first();
}
public function verifyPassword(string $password, string $hash): bool
{
return password_verify($password, $hash);
}
}