init backend presensi

This commit is contained in:
mwpn
2026-03-05 14:37:36 +07:00
commit b4fda6b9c9
319 changed files with 27261 additions and 0 deletions

View File

@@ -0,0 +1 @@
# Devices Module - Controllers

View File

@@ -0,0 +1,72 @@
<?php
namespace App\Modules\Devices\Controllers;
use App\Core\BaseApiController;
use App\Modules\Devices\Services\DeviceAuthService;
use CodeIgniter\HTTP\ResponseInterface;
/**
* Device Authentication Controller
*
* Handles device authentication endpoints.
*/
class DeviceAuthController extends BaseApiController
{
protected DeviceAuthService $deviceAuthService;
public function __construct()
{
$this->deviceAuthService = new DeviceAuthService();
}
/**
* Device login endpoint
*
* Authenticates device using device_code and api_key.
*
* POST /api/device/login
* Body: { "device_code": "", "api_key": "" }
*
* @return ResponseInterface
*/
public function login(): ResponseInterface
{
// Get JSON input
$input = $this->request->getJSON(true);
// Validate input
if (empty($input['device_code']) || empty($input['api_key'])) {
return $this->errorResponse(
'device_code and api_key are required',
null,
null,
400
);
}
// Authenticate device
$deviceData = $this->deviceAuthService->authenticate(
$input['device_code'],
$input['api_key']
);
if (!$deviceData) {
return $this->errorResponse(
'Invalid device credentials',
null,
null,
401
);
}
// Return success response
return $this->successResponse(
[
'device_id' => $deviceData['device_id'],
'device_code' => $deviceData['device_code'],
],
'Device authenticated'
);
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace App\Modules\Devices\Controllers;
use App\Core\BaseApiController;
use App\Modules\Devices\Models\DeviceModel;
use CodeIgniter\HTTP\ResponseInterface;
/**
* Device Controller
*
* Admin-only management for device configuration (geo-fence, etc).
*/
class DeviceController extends BaseApiController
{
/**
* PUT /api/devices/{id}
*
* Body (JSON):
* - latitude: float|null
* - longitude: float|null
* - radius_meters: int|null
*/
public function update($id): ResponseInterface
{
$id = (int) $id;
if ($id <= 0) {
return $this->errorResponse('Invalid device id', null, null, ResponseInterface::HTTP_BAD_REQUEST);
}
$payload = $this->request->getJSON(true) ?? [];
$lat = $payload['latitude'] ?? null;
$lng = $payload['longitude'] ?? null;
$radius = $payload['radius_meters'] ?? null;
// Normalize empty strings to null
$lat = ($lat === '' || $lat === null) ? null : $lat;
$lng = ($lng === '' || $lng === null) ? null : $lng;
$radius = ($radius === '' || $radius === null) ? null : $radius;
// Basic validation
if ($lat !== null && !is_numeric($lat)) {
return $this->errorResponse('Latitude harus berupa angka atau kosong', null, null, ResponseInterface::HTTP_UNPROCESSABLE_ENTITY);
}
if ($lng !== null && !is_numeric($lng)) {
return $this->errorResponse('Longitude harus berupa angka atau kosong', null, null, ResponseInterface::HTTP_UNPROCESSABLE_ENTITY);
}
if ($radius !== null && (!is_numeric($radius) || (int) $radius < 0)) {
return $this->errorResponse('Radius harus berupa angka >= 0 atau kosong', null, null, ResponseInterface::HTTP_UNPROCESSABLE_ENTITY);
}
// If one of lat/lng/radius set, require all three
$hasAny = $lat !== null || $lng !== null || $radius !== null;
if ($hasAny) {
if ($lat === null || $lng === null || $radius === null) {
return $this->errorResponse('Jika mengatur zona, latitude, longitude, dan radius wajib diisi semua', null, null, ResponseInterface::HTTP_UNPROCESSABLE_ENTITY);
}
}
$data = [
'latitude' => $lat !== null ? (float) $lat : null,
'longitude' => $lng !== null ? (float) $lng : null,
'radius_meters' => $radius !== null ? (int) $radius : null,
];
$model = new DeviceModel();
$device = $model->find($id);
if (!$device) {
return $this->errorResponse('Device tidak ditemukan', null, null, ResponseInterface::HTTP_NOT_FOUND);
}
if (!$model->update($id, $data)) {
return $this->errorResponse('Gagal menyimpan konfigurasi device', $model->errors(), null, ResponseInterface::HTTP_UNPROCESSABLE_ENTITY);
}
return $this->successResponse(null, 'Konfigurasi geo-fence device berhasil disimpan');
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Modules\Devices\Controllers;
use App\Core\BaseApiController;
use App\Modules\Geo\Models\ZoneModel;
use CodeIgniter\HTTP\ResponseInterface;
/**
* Mobile Support Controller
*
* Endpoints for Android app (ping, bootstrap). No authentication required.
*/
class MobileController extends BaseApiController
{
/**
* Default check-in late tolerance in minutes (must match AttendanceCheckinService)
*/
protected int $checkinToleranceMinutes = 10;
/**
* GET /api/mobile/ping
*
* @return ResponseInterface
*/
public function ping(): ResponseInterface
{
$data = [
'server_time' => date('Y-m-d H:i:s'),
'api_version' => '1.0',
];
return $this->successResponse($data, 'Mobile connected');
}
/**
* GET /api/mobile/bootstrap
*
* Returns active zones, device validation rules, and checkin tolerance for app startup.
*
* @return ResponseInterface
*/
public function bootstrap(): ResponseInterface
{
$zoneModel = new ZoneModel();
$activeZones = $zoneModel->findAllActive();
$zonesData = [];
foreach ($activeZones as $zone) {
$zonesData[] = [
'zone_code' => $zone->zone_code,
'zone_name' => $zone->zone_name,
'latitude' => (float) $zone->latitude,
'longitude' => (float) $zone->longitude,
'radius_meters' => (int) $zone->radius_meters,
];
}
$data = [
'server_timestamp' => gmdate('Y-m-d\TH:i:s\Z'), // ISO 8601 UTC — for device_time_offset = server_time - device_time
'server_timezone' => 'Asia/Jakarta', // WIB — for display / jadwal sekolah
'active_zones' => $zonesData,
'device_validation_rules' => [
'require_device_code' => true,
'require_api_key' => true,
],
'checkin_tolerance_minutes' => $this->checkinToleranceMinutes,
];
return $this->successResponse($data, 'Bootstrap data');
}
}

View File

@@ -0,0 +1 @@
# Devices Module - Entities

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Modules\Devices\Entities;
use CodeIgniter\Entity\Entity;
/**
* Device Entity
*
* Represents a device in the system.
*/
class Device extends Entity
{
/**
* Attributes that can be mass assigned
*
* @var array<string>
*/
protected $allowedFields = [
'device_code',
'device_name',
'api_key',
'is_active',
'last_seen_at',
];
/**
* Attributes that should be cast to specific types
*
* @var array<string, string>
*/
protected $casts = [
'id' => 'integer',
'is_active' => 'boolean',
'last_seen_at' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Check if device is active
*
* @return bool
*/
public function isActive(): bool
{
return (bool) $this->attributes['is_active'];
}
}

View File

@@ -0,0 +1 @@
# Devices Module - Models

View File

@@ -0,0 +1,101 @@
<?php
namespace App\Modules\Devices\Models;
use App\Modules\Devices\Entities\Device;
use CodeIgniter\Model;
/**
* Device Model
*
* Handles database operations for devices.
*/
class DeviceModel extends Model
{
protected $table = 'devices';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $returnType = Device::class;
protected $useSoftDeletes = false;
protected $protectFields = true;
protected $allowedFields = [
'device_code',
'device_name',
'api_key',
'is_active',
'last_seen_at',
'latitude',
'longitude',
'radius_meters',
];
// Dates
protected $useTimestamps = true;
protected $dateFormat = 'datetime';
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $deletedField = 'deleted_at';
// Validation
protected $validationRules = [
'device_code' => 'required|max_length[100]|is_unique[devices.device_code,id,{id}]',
'device_name' => 'permit_empty|max_length[255]',
'api_key' => 'required|max_length[255]',
'is_active' => 'permit_empty|in_list[0,1]',
'latitude' => 'permit_empty|decimal',
'longitude' => 'permit_empty|decimal',
'radius_meters' => 'permit_empty|integer|greater_than_equal_to[0]',
];
protected $validationMessages = [];
protected $skipValidation = false;
protected $cleanValidationRules = true;
// Callbacks
protected $allowCallbacks = true;
protected $beforeInsert = [];
protected $afterInsert = [];
protected $beforeUpdate = [];
protected $afterUpdate = [];
protected $beforeFind = [];
protected $afterFind = [];
protected $beforeDelete = [];
protected $afterDelete = [];
/**
* Find device by device_code
*
* @param string $deviceCode
* @return Device|null
*/
public function findByDeviceCode(string $deviceCode): ?Device
{
return $this->where('device_code', $deviceCode)->first();
}
/**
* Find active device by device_code
*
* @param string $deviceCode
* @return Device|null
*/
public function findActiveByDeviceCode(string $deviceCode): ?Device
{
return $this->where('device_code', $deviceCode)
->where('is_active', 1)
->first();
}
/**
* Update last_seen_at timestamp
*
* @param int $deviceId
* @return bool
*/
public function updateLastSeen(int $deviceId): bool
{
return $this->update($deviceId, [
'last_seen_at' => date('Y-m-d H:i:s'),
]);
}
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* Devices Module Routes
*
* This file is automatically loaded by ModuleLoader.
* Define your device management routes here.
*
* @var RouteCollection $routes
*/
// Device authentication routes
$routes->group('api/device', ['namespace' => 'App\Modules\Devices\Controllers'], function ($routes) {
$routes->post('login', 'DeviceAuthController::login');
});
// Mobile support routes (no auth required)
$routes->group('api/mobile', ['namespace' => 'App\Modules\Devices\Controllers'], function ($routes) {
$routes->get('ping', 'MobileController::ping');
$routes->get('bootstrap', 'MobileController::bootstrap');
});
// Device management (admin only)
$routes->group('api/devices', [
'namespace' => 'App\Modules\Devices\Controllers',
'filter' => 'admin_only',
], function ($routes) {
$routes->put('(:num)', 'DeviceController::update/$1');
});
// Example route structure (uncomment and modify as needed):
// $routes->group('api/devices', ['namespace' => 'App\Modules\Devices\Controllers'], function($routes) {
// $routes->get('/', 'DeviceController::index');
// $routes->get('(:num)', 'DeviceController::show/$1');
// $routes->post('/', 'DeviceController::register');
// $routes->put('(:num)', 'DeviceController::update/$1');
// $routes->delete('(:num)', 'DeviceController::delete/$1');
// });

View File

@@ -0,0 +1 @@
# Devices Module - Services

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Modules\Devices\Services;
use App\Modules\Devices\Models\DeviceModel;
/**
* Device Authentication Service
*
* Handles device authentication logic.
*/
class DeviceAuthService
{
protected DeviceModel $deviceModel;
public function __construct()
{
$this->deviceModel = new DeviceModel();
}
/**
* Authenticate device by device_code and api_key
*
* @param string $deviceCode Device code
* @param string $apiKey API key
* @return array|null Returns device data if authenticated, null otherwise
*/
public function authenticate(string $deviceCode, string $apiKey): ?array
{
// Find active device by device_code
$device = $this->deviceModel->findActiveByDeviceCode($deviceCode);
if (!$device) {
return null;
}
// For development: compare plain api_key
// In production, should use password_verify() with hashed api_key
if ($device->api_key !== $apiKey) {
return null;
}
// Update last_seen_at
$this->touchLastSeen($device->id);
return [
'device_id' => $device->id,
'device_code' => $device->device_code,
'device_name' => $device->device_name,
'latitude' => $device->latitude !== null ? (float) $device->latitude : null,
'longitude' => $device->longitude !== null ? (float) $device->longitude : null,
'radius_meters' => $device->radius_meters !== null ? (int) $device->radius_meters : null,
];
}
/**
* Update last_seen_at timestamp for device
*
* @param int $deviceId Device ID
* @return void
*/
public function touchLastSeen(int $deviceId): void
{
$this->deviceModel->updateLastSeen($deviceId);
}
}