init backend presensi
This commit is contained in:
1
app/Modules/Devices/Controllers/.gitkeep
Normal file
1
app/Modules/Devices/Controllers/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
# Devices Module - Controllers
|
||||
72
app/Modules/Devices/Controllers/DeviceAuthController.php
Normal file
72
app/Modules/Devices/Controllers/DeviceAuthController.php
Normal 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'
|
||||
);
|
||||
}
|
||||
}
|
||||
80
app/Modules/Devices/Controllers/DeviceController.php
Normal file
80
app/Modules/Devices/Controllers/DeviceController.php
Normal 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');
|
||||
}
|
||||
}
|
||||
|
||||
71
app/Modules/Devices/Controllers/MobileController.php
Normal file
71
app/Modules/Devices/Controllers/MobileController.php
Normal 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');
|
||||
}
|
||||
}
|
||||
1
app/Modules/Devices/Entities/.gitkeep
Normal file
1
app/Modules/Devices/Entities/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
# Devices Module - Entities
|
||||
49
app/Modules/Devices/Entities/Device.php
Normal file
49
app/Modules/Devices/Entities/Device.php
Normal 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'];
|
||||
}
|
||||
}
|
||||
1
app/Modules/Devices/Models/.gitkeep
Normal file
1
app/Modules/Devices/Models/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
# Devices Module - Models
|
||||
101
app/Modules/Devices/Models/DeviceModel.php
Normal file
101
app/Modules/Devices/Models/DeviceModel.php
Normal 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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
38
app/Modules/Devices/Routes.php
Normal file
38
app/Modules/Devices/Routes.php
Normal 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');
|
||||
// });
|
||||
1
app/Modules/Devices/Services/.gitkeep
Normal file
1
app/Modules/Devices/Services/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
# Devices Module - Services
|
||||
66
app/Modules/Devices/Services/DeviceAuthService.php
Normal file
66
app/Modules/Devices/Services/DeviceAuthService.php
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user