feat: add audit log query endpoint
This commit is contained in:
parent
51fa8c1949
commit
a73b88bc05
@ -14,8 +14,9 @@ $routes->options('(:any)', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$routes->group('api', ['filter' => 'auth'], function ($routes) {
|
$routes->group('api', ['filter' => 'auth'], function ($routes) {
|
||||||
$routes->get('dashboard', 'DashboardController::index');
|
$routes->get('dashboard', 'DashboardController::index');
|
||||||
$routes->get('sample', 'SampleController::index');
|
$routes->get('sample', 'SampleController::index');
|
||||||
|
$routes->get('audit-logs', 'Audit\AuditLogController::index');
|
||||||
|
|
||||||
// Results CRUD
|
// Results CRUD
|
||||||
$routes->group('result', function ($routes) {
|
$routes->group('result', function ($routes) {
|
||||||
|
|||||||
60
app/Controllers/Audit/AuditLogController.php
Normal file
60
app/Controllers/Audit/AuditLogController.php
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers\Audit;
|
||||||
|
|
||||||
|
use App\Controllers\BaseController;
|
||||||
|
use App\Services\AuditLogService;
|
||||||
|
use App\Traits\ResponseTrait;
|
||||||
|
use CodeIgniter\HTTP\ResponseInterface;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
class AuditLogController extends BaseController
|
||||||
|
{
|
||||||
|
use ResponseTrait;
|
||||||
|
|
||||||
|
private AuditLogService $auditLogService;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->auditLogService = new AuditLogService();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(): ResponseInterface
|
||||||
|
{
|
||||||
|
$filters = [
|
||||||
|
'table' => $this->request->getGet('table'),
|
||||||
|
'rec_id' => $this->request->getGet('rec_id') ?? $this->request->getGet('recId'),
|
||||||
|
'event_id' => $this->request->getGet('event_id') ?? $this->request->getGet('eventId'),
|
||||||
|
'activity_id' => $this->request->getGet('activity_id') ?? $this->request->getGet('activityId'),
|
||||||
|
'from' => $this->request->getGet('from'),
|
||||||
|
'to' => $this->request->getGet('to'),
|
||||||
|
'search' => $this->request->getGet('search'),
|
||||||
|
'page' => $this->request->getGet('page'),
|
||||||
|
'perPage' => $this->request->getGet('perPage') ?? $this->request->getGet('per_page'),
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$payload = $this->auditLogService->fetchLogs($filters);
|
||||||
|
|
||||||
|
return $this->respond([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => 'Audit logs retrieved successfully',
|
||||||
|
'data' => $payload,
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
} catch (InvalidArgumentException $e) {
|
||||||
|
return $this->respond([
|
||||||
|
'status' => 'failed',
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'data' => null,
|
||||||
|
], 400);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
log_message('error', 'AuditLogController::index error: ' . $e->getMessage());
|
||||||
|
return $this->respond([
|
||||||
|
'status' => 'failed',
|
||||||
|
'message' => 'Unable to retrieve audit logs',
|
||||||
|
'data' => null,
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
179
app/Services/AuditLogService.php
Normal file
179
app/Services/AuditLogService.php
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use CodeIgniter\Database\BaseConnection;
|
||||||
|
use DateTime;
|
||||||
|
use DateTimeZone;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class AuditLogService
|
||||||
|
{
|
||||||
|
private const TABLE_MAP = [
|
||||||
|
'logpatient' => 'logpatient',
|
||||||
|
'patient' => 'logpatient',
|
||||||
|
'visit' => 'logpatient',
|
||||||
|
'logorder' => 'logorder',
|
||||||
|
'order' => 'logorder',
|
||||||
|
'specimen' => 'logorder',
|
||||||
|
'result' => 'logorder',
|
||||||
|
'logmaster' => 'logmaster',
|
||||||
|
'master' => 'logmaster',
|
||||||
|
'config' => 'logmaster',
|
||||||
|
'valueset' => 'logmaster',
|
||||||
|
'logsystem' => 'logsystem',
|
||||||
|
'system' => 'logsystem',
|
||||||
|
'auth' => 'logsystem',
|
||||||
|
'job' => 'logsystem',
|
||||||
|
];
|
||||||
|
|
||||||
|
private const PRIMARY_KEYS = [
|
||||||
|
'logpatient' => 'LogPatientID',
|
||||||
|
'logorder' => 'LogOrderID',
|
||||||
|
'logmaster' => 'LogMasterID',
|
||||||
|
'logsystem' => 'LogSystemID',
|
||||||
|
];
|
||||||
|
|
||||||
|
private const DEFAULT_PAGE = 1;
|
||||||
|
private const DEFAULT_PER_PAGE = 20;
|
||||||
|
private const MAX_PER_PAGE = 100;
|
||||||
|
|
||||||
|
private static ?BaseConnection $db = null;
|
||||||
|
|
||||||
|
public function fetchLogs(array $filters): array
|
||||||
|
{
|
||||||
|
$tableKey = $filters['table'] ?? null;
|
||||||
|
if (empty($tableKey)) {
|
||||||
|
throw new InvalidArgumentException('table parameter is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$logTable = $this->resolveLogTable($tableKey);
|
||||||
|
if ($logTable === null) {
|
||||||
|
throw new InvalidArgumentException("Unknown audit table: {$tableKey}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$builder = $this->getDb()->table($logTable);
|
||||||
|
|
||||||
|
$this->applyFilters($builder, $filters);
|
||||||
|
|
||||||
|
$total = (int) $builder->countAllResults(false);
|
||||||
|
|
||||||
|
$page = $this->normalizePage($filters['page'] ?? null);
|
||||||
|
$perPage = $this->normalizePerPage($filters['perPage'] ?? $filters['per_page'] ?? null);
|
||||||
|
$offset = ($page - 1) * $perPage;
|
||||||
|
|
||||||
|
$builder->orderBy('LogDate', 'DESC');
|
||||||
|
$builder->orderBy($this->getPrimaryKey($logTable), 'DESC');
|
||||||
|
|
||||||
|
$rows = $builder
|
||||||
|
->limit($perPage, $offset)
|
||||||
|
->get()
|
||||||
|
->getResultArray();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'data' => $rows,
|
||||||
|
'pagination' => [
|
||||||
|
'page' => $page,
|
||||||
|
'perPage' => $perPage,
|
||||||
|
'total' => $total,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function applyFilters($builder, array $filters): void
|
||||||
|
{
|
||||||
|
if (!empty($filters['rec_id'])) {
|
||||||
|
$builder->where('RecID', (string) $filters['rec_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($filters['event_id'])) {
|
||||||
|
$builder->where('EventID', $this->normalizeCode($filters['event_id']));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($filters['activity_id'])) {
|
||||||
|
$builder->where('ActivityID', $this->normalizeCode($filters['activity_id']));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->applyDateRange($builder, $filters['from'] ?? null, $filters['to'] ?? null);
|
||||||
|
|
||||||
|
if (!empty($filters['search'])) {
|
||||||
|
$search = trim($filters['search']);
|
||||||
|
if ($search !== '') {
|
||||||
|
$builder->groupStart();
|
||||||
|
$builder->like('UserID', $search);
|
||||||
|
$builder->orLike('Reason', $search);
|
||||||
|
$builder->orLike('FldName', $search);
|
||||||
|
$builder->orLike('FldValuePrev', $search);
|
||||||
|
$builder->orLike('FldValueNew', $search);
|
||||||
|
$builder->orLike('EventID', $search);
|
||||||
|
$builder->orLike('ActivityID', $search);
|
||||||
|
$builder->groupEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function applyDateRange($builder, ?string $from, ?string $to): void
|
||||||
|
{
|
||||||
|
if ($from !== null && trim($from) !== '') {
|
||||||
|
$builder->where('LogDate >=', $this->normalizeDate($from));
|
||||||
|
}
|
||||||
|
if ($to !== null && trim($to) !== '') {
|
||||||
|
$builder->where('LogDate <=', $this->normalizeDate($to));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeDate(string $value): string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$dt = new DateTime($value, new DateTimeZone('UTC'));
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
throw new InvalidArgumentException('Invalid date: ' . $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $dt->format('Y-m-d H:i:s');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeCode(string $value): string
|
||||||
|
{
|
||||||
|
return strtoupper(trim($value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizePage($value): int
|
||||||
|
{
|
||||||
|
$page = (int) ($value ?? self::DEFAULT_PAGE);
|
||||||
|
return $page < 1 ? self::DEFAULT_PAGE : $page;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizePerPage($value): int
|
||||||
|
{
|
||||||
|
$perPage = (int) ($value ?? self::DEFAULT_PER_PAGE);
|
||||||
|
if ($perPage < 1) {
|
||||||
|
return self::DEFAULT_PER_PAGE;
|
||||||
|
}
|
||||||
|
if ($perPage > self::MAX_PER_PAGE) {
|
||||||
|
throw new InvalidArgumentException('perPage cannot be greater than ' . self::MAX_PER_PAGE);
|
||||||
|
}
|
||||||
|
return $perPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveLogTable(?string $key): ?string
|
||||||
|
{
|
||||||
|
if ($key === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lookup = strtolower(trim($key));
|
||||||
|
return self::TABLE_MAP[$lookup] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getPrimaryKey(string $table): string
|
||||||
|
{
|
||||||
|
return self::PRIMARY_KEYS[$table] ?? 'LogID';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDb(): BaseConnection
|
||||||
|
{
|
||||||
|
return self::$db ??= \Config\Database::connect();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -57,7 +57,86 @@ tags:
|
|||||||
description: User management and administration
|
description: User management and administration
|
||||||
- name: Demo
|
- name: Demo
|
||||||
description: Demo/test endpoints (no authentication)
|
description: Demo/test endpoints (no authentication)
|
||||||
|
- name: Audit
|
||||||
|
description: Audit log retrieval and filtering
|
||||||
paths:
|
paths:
|
||||||
|
/api/audit-logs:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Audit
|
||||||
|
summary: Retrieve audit log entries for a table
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- name: table
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Table alias for the audit data (logpatient, logorder, logmaster, logsystem)
|
||||||
|
- name: rec_id
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Primary record identifier (RecID) to filter audit rows
|
||||||
|
- name: event_id
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Canonical EventID (case insensitive)
|
||||||
|
- name: activity_id
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Canonical ActivityID (case insensitive)
|
||||||
|
- name: from
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: Lower bound for LogDate inclusive
|
||||||
|
- name: to
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: Upper bound for LogDate inclusive
|
||||||
|
- name: search
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Search term that matches user, reason, field names, or values
|
||||||
|
- name: page
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
default: 1
|
||||||
|
description: Page number
|
||||||
|
- name: perPage
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
default: 20
|
||||||
|
description: Items per page (max 100)
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Audit log results
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AuditLogsEnvelope'
|
||||||
|
'400':
|
||||||
|
description: Validation failure (missing table or invalid filters)
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AuditLogsErrorResponse'
|
||||||
|
'500':
|
||||||
|
description: Internal error when retrieving audit logs
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AuditLogsErrorResponse'
|
||||||
/api/auth/login:
|
/api/auth/login:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
@ -7874,6 +7953,121 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
nullable: true
|
nullable: true
|
||||||
|
AuditLogEntry:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
LogPatientID:
|
||||||
|
type: integer
|
||||||
|
nullable: true
|
||||||
|
LogOrderID:
|
||||||
|
type: integer
|
||||||
|
nullable: true
|
||||||
|
LogMasterID:
|
||||||
|
type: integer
|
||||||
|
nullable: true
|
||||||
|
LogSystemID:
|
||||||
|
type: integer
|
||||||
|
nullable: true
|
||||||
|
TblName:
|
||||||
|
type: string
|
||||||
|
RecID:
|
||||||
|
type: string
|
||||||
|
FldName:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
FldValuePrev:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
FldValueNew:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
UserID:
|
||||||
|
type: string
|
||||||
|
SiteID:
|
||||||
|
type: string
|
||||||
|
DIDType:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
DID:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
MachineID:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
SessionID:
|
||||||
|
type: string
|
||||||
|
AppID:
|
||||||
|
type: string
|
||||||
|
ProcessID:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
WebPageID:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
EventID:
|
||||||
|
type: string
|
||||||
|
ActivityID:
|
||||||
|
type: string
|
||||||
|
Reason:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
LogDate:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
Context:
|
||||||
|
type: string
|
||||||
|
IpAddress:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
AuditLogListResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/AuditLogEntry'
|
||||||
|
pagination:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
page:
|
||||||
|
type: integer
|
||||||
|
perPage:
|
||||||
|
type: integer
|
||||||
|
total:
|
||||||
|
type: integer
|
||||||
|
required:
|
||||||
|
- page
|
||||||
|
- perPage
|
||||||
|
- total
|
||||||
|
required:
|
||||||
|
- data
|
||||||
|
- pagination
|
||||||
|
AuditLogsEnvelope:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
data:
|
||||||
|
$ref: '#/components/schemas/AuditLogListResponse'
|
||||||
|
required:
|
||||||
|
- status
|
||||||
|
- message
|
||||||
|
- data
|
||||||
|
AuditLogsErrorResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
data:
|
||||||
|
nullable: true
|
||||||
|
required:
|
||||||
|
- status
|
||||||
|
- message
|
||||||
|
- data
|
||||||
Contact:
|
Contact:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
@ -59,6 +59,8 @@ tags:
|
|||||||
description: User management and administration
|
description: User management and administration
|
||||||
- name: Demo
|
- name: Demo
|
||||||
description: Demo/test endpoints (no authentication)
|
description: Demo/test endpoints (no authentication)
|
||||||
|
- name: Audit
|
||||||
|
description: Audit log retrieval and filtering
|
||||||
|
|
||||||
components:
|
components:
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
|
|||||||
107
public/components/schemas/audit-logs.yaml
Normal file
107
public/components/schemas/audit-logs.yaml
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
AuditLogEntry:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
LogPatientID:
|
||||||
|
type: integer
|
||||||
|
nullable: true
|
||||||
|
LogOrderID:
|
||||||
|
type: integer
|
||||||
|
nullable: true
|
||||||
|
LogMasterID:
|
||||||
|
type: integer
|
||||||
|
nullable: true
|
||||||
|
LogSystemID:
|
||||||
|
type: integer
|
||||||
|
nullable: true
|
||||||
|
TblName:
|
||||||
|
type: string
|
||||||
|
RecID:
|
||||||
|
type: string
|
||||||
|
FldName:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
FldValuePrev:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
FldValueNew:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
UserID:
|
||||||
|
type: string
|
||||||
|
SiteID:
|
||||||
|
type: string
|
||||||
|
DIDType:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
DID:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
MachineID:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
SessionID:
|
||||||
|
type: string
|
||||||
|
AppID:
|
||||||
|
type: string
|
||||||
|
ProcessID:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
WebPageID:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
EventID:
|
||||||
|
type: string
|
||||||
|
ActivityID:
|
||||||
|
type: string
|
||||||
|
Reason:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
LogDate:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
Context:
|
||||||
|
type: string
|
||||||
|
IpAddress:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
|
||||||
|
AuditLogListResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/AuditLogEntry'
|
||||||
|
pagination:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
page:
|
||||||
|
type: integer
|
||||||
|
perPage:
|
||||||
|
type: integer
|
||||||
|
total:
|
||||||
|
type: integer
|
||||||
|
required: [page, perPage, total]
|
||||||
|
required: [data, pagination]
|
||||||
|
|
||||||
|
AuditLogsEnvelope:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
data:
|
||||||
|
$ref: '#/AuditLogListResponse'
|
||||||
|
required: [status, message, data]
|
||||||
|
|
||||||
|
AuditLogsErrorResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
data:
|
||||||
|
nullable: true
|
||||||
|
required: [status, message, data]
|
||||||
76
public/paths/audit-logs.yaml
Normal file
76
public/paths/audit-logs.yaml
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/api/audit-logs:
|
||||||
|
get:
|
||||||
|
tags: [Audit]
|
||||||
|
summary: Retrieve audit log entries for a table
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- name: table
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Table alias for the audit data (logpatient, logorder, logmaster, logsystem)
|
||||||
|
- name: rec_id
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Primary record identifier (RecID) to filter audit rows
|
||||||
|
- name: event_id
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Canonical EventID (case insensitive)
|
||||||
|
- name: activity_id
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Canonical ActivityID (case insensitive)
|
||||||
|
- name: from
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: Lower bound for LogDate inclusive
|
||||||
|
- name: to
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: Upper bound for LogDate inclusive
|
||||||
|
- name: search
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Search term that matches user, reason, field names, or values
|
||||||
|
- name: page
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
default: 1
|
||||||
|
description: Page number
|
||||||
|
- name: perPage
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
default: 20
|
||||||
|
description: Items per page (max 100)
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Audit log results
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../components/schemas/audit-logs.yaml#/AuditLogsEnvelope'
|
||||||
|
'400':
|
||||||
|
description: Validation failure (missing table or invalid filters)
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../components/schemas/audit-logs.yaml#/AuditLogsErrorResponse'
|
||||||
|
'500':
|
||||||
|
description: Internal error when retrieving audit logs
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../components/schemas/audit-logs.yaml#/AuditLogsErrorResponse'
|
||||||
117
tests/feature/Audit/AuditLogTest.php
Normal file
117
tests/feature/Audit/AuditLogTest.php
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Audit;
|
||||||
|
|
||||||
|
use CodeIgniter\Test\FeatureTestTrait;
|
||||||
|
use CodeIgniter\Test\CIUnitTestCase;
|
||||||
|
use Firebase\JWT\JWT;
|
||||||
|
|
||||||
|
class AuditLogTest extends CIUnitTestCase
|
||||||
|
{
|
||||||
|
use FeatureTestTrait;
|
||||||
|
|
||||||
|
protected $db;
|
||||||
|
private $testRecId = 'TEST-REC-123';
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->db = \Config\Database::connect();
|
||||||
|
$this->db->table('logpatient')->insert([
|
||||||
|
'TblName' => 'patient',
|
||||||
|
'RecID' => $this->testRecId,
|
||||||
|
'UserID' => 'USR_TEST',
|
||||||
|
'SiteID' => 'SITE01',
|
||||||
|
'SessionID' => 'sess_test',
|
||||||
|
'AppID' => 'clqms-api',
|
||||||
|
'EventID' => 'PATIENT_REGISTERED',
|
||||||
|
'ActivityID' => 'CREATE',
|
||||||
|
'LogDate' => '2026-03-25 12:00:00',
|
||||||
|
'Context' => json_encode([
|
||||||
|
'request_id' => 'req-test-1',
|
||||||
|
'route' => 'POST /api/patient',
|
||||||
|
'timestamp_utc' => '2026-03-25T12:00:00.000Z',
|
||||||
|
'entity_type' => 'patient',
|
||||||
|
'entity_version' => 1,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
$this->db->table('logpatient')->where('RecID', $this->testRecId)->delete();
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTableIsRequired()
|
||||||
|
{
|
||||||
|
$result = $this->getWithAuth('api/audit-logs');
|
||||||
|
|
||||||
|
$result->assertStatus(400);
|
||||||
|
$result->assertJSONFragment([
|
||||||
|
'status' => 'failed',
|
||||||
|
'message' => 'table parameter is required',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUnknownTableReturnsValidationError()
|
||||||
|
{
|
||||||
|
$result = $this->getWithAuth('api/audit-logs?table=unknown');
|
||||||
|
|
||||||
|
$result->assertStatus(400);
|
||||||
|
$result->assertJSONFragment([
|
||||||
|
'status' => 'failed',
|
||||||
|
'message' => 'Unknown audit table: unknown',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAuditLogsFilterByRecId()
|
||||||
|
{
|
||||||
|
$result = $this->getWithAuth('api/audit-logs?table=logpatient&rec_id=' . $this->testRecId);
|
||||||
|
|
||||||
|
$result->assertStatus(200);
|
||||||
|
$result->assertJSONFragment([
|
||||||
|
'status' => 'success',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$payload = json_decode($result->getJSON(), true);
|
||||||
|
$this->assertCount(1, $payload['data']['data']);
|
||||||
|
$this->assertEquals($this->testRecId, $payload['data']['data'][0]['RecID']);
|
||||||
|
|
||||||
|
$pagination = $payload['data']['pagination'];
|
||||||
|
$this->assertSame(1, $pagination['page']);
|
||||||
|
$this->assertSame(20, $pagination['perPage']);
|
||||||
|
$this->assertSame(1, $pagination['total']);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getWithAuth(string $uri)
|
||||||
|
{
|
||||||
|
$_COOKIE['token'] = $this->buildToken();
|
||||||
|
|
||||||
|
$response = $this->get($uri);
|
||||||
|
|
||||||
|
unset($_COOKIE['token']);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildToken(): string
|
||||||
|
{
|
||||||
|
$payload = [
|
||||||
|
'sub' => 'audit-test',
|
||||||
|
'iat' => time(),
|
||||||
|
];
|
||||||
|
|
||||||
|
return JWT::encode($payload, $this->resolveSecret(), 'HS256');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveSecret(): string
|
||||||
|
{
|
||||||
|
$secret = getenv('JWT_SECRET');
|
||||||
|
if ($secret === false) {
|
||||||
|
return 'tests-secret';
|
||||||
|
}
|
||||||
|
return trim($secret, "'\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user