diff --git a/app/Controllers/Authenticate/AuthenticatePostValidator.php b/app/Controllers/Authenticate/AuthenticatePostValidator.php
index d69fe04..9e64391 100644
--- a/app/Controllers/Authenticate/AuthenticatePostValidator.php
+++ b/app/Controllers/Authenticate/AuthenticatePostValidator.php
@@ -13,7 +13,7 @@
class AuthenticatePostValidator
{
- private const ALLOWED = ['username', 'password', 'id'];
+ private const ALLOWED = ['username', 'password', 'id', 'uploaded_files'];
/**
* @throws JsonException
@@ -40,7 +40,7 @@ public function __invoke(Request $request, RequestHandler $handler): ResponseInt
}
// id can be part of the request, but it MUST be null/empty
- if (V::exists()->validate($parsedRequest['id']) && V::notEmpty()->validate($parsedRequest['id'])) {
+ if (V::key('id')->validate($parsedRequest) && V::notEmpty()->validate($parsedRequest['id'])) {
$responseBody->registerParam('invalid', 'id', 'null');
}
diff --git a/app/Controllers/DeleteActionBase.php b/app/Controllers/DeleteActionBase.php
index 21c6f13..8c3bc31 100644
--- a/app/Controllers/DeleteActionBase.php
+++ b/app/Controllers/DeleteActionBase.php
@@ -23,13 +23,19 @@ abstract class DeleteActionBase extends ActionBase
public function __invoke(Request $request, Response $response, array $args): ResponseInterface {
/** @var ResponseBody $responseBody */
$responseBody = $request->getAttribute('response_body');
+ $parsedRequest = $responseBody->getParsedRequest();
$model = $this->model;
+ $id = $args['id'];
+ $model = $model->find($id);
- // Destroy the model given the id.
- if ($model::destroy($args['id']) === 1) {
- $status = ResponseCodes::HTTP_OK;
- } else {
- $status = ResponseCodes::HTTP_NOT_FOUND;
+ $status = ResponseCodes::HTTP_NOT_FOUND;
+ if ($model !== null) {
+ if (array_key_exists('force', $parsedRequest) && $parsedRequest['force'] === "true") {
+ $isDeleted = $model->forceDelete();
+ } else {
+ $isDeleted = $model->delete();
+ }
+ $status= $isDeleted ? ResponseCodes::HTTP_OK : ResponseCodes::HTTP_INTERNAL_SERVER_ERROR;
}
// Set the status and data of the ResponseBody
diff --git a/app/Controllers/File/FileController.php b/app/Controllers/File/FileController.php
new file mode 100644
index 0000000..ea0f6e5
--- /dev/null
+++ b/app/Controllers/File/FileController.php
@@ -0,0 +1,29 @@
+post('/file/upload/{client_id}', FileUploadAction::class)
+ ->add(FileUploadValidator::class);
+
+ $group->get('/file/download/{id}', FileDownloadAction::class);
+
+ $group->get('/file/{id}', FileGetAction::class);
+
+ $group->post('/file', FileUpdateAction::class);
+
+ $group->delete('/file/{id}', FileDeleteAction::class);
+
+ $group->get('/file/load/{client_id}', FileLoadAction::class);
+ }
+}
diff --git a/app/Controllers/File/FileDeleteAction.php b/app/Controllers/File/FileDeleteAction.php
new file mode 100644
index 0000000..479c8ae
--- /dev/null
+++ b/app/Controllers/File/FileDeleteAction.php
@@ -0,0 +1,18 @@
+model = $model;
+ }
+}
diff --git a/app/Controllers/File/FileDownloadAction.php b/app/Controllers/File/FileDownloadAction.php
new file mode 100644
index 0000000..3e00eec
--- /dev/null
+++ b/app/Controllers/File/FileDownloadAction.php
@@ -0,0 +1,61 @@
+getAttribute('response_body');
+
+
+ /**
+ * Load the File Model with the given id (PK)
+ * @var File|FileRepresentation|null $fileModel
+ */
+ $fileModel = $this->file->makeVisible('Image')->find($args['id']);
+
+ // If the record is not found then 404 error, otherwise status is 200.
+ if ($fileModel === null) {
+ $responseBody = $responseBody
+ ->setData(null)
+ ->setStatus(ResponseCodes::HTTP_NOT_FOUND);
+ return $responseBody();
+ }
+
+ return $response
+ ->withHeader('Content-Type', 'application/octet-stream')
+ ->withHeader('Content-Disposition', 'attachment; filename="' . $fileModel->FileName . '"')
+ ->withAddedHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
+ ->withHeader('Cache-Control', 'post-check=0, pre-check=0')
+ ->withHeader('Pragma', 'no-cache')
+ ->withBody((new StreamFactory())->createStream($fileModel->Image));
+ }
+}
diff --git a/app/Controllers/File/FileGetAction.php b/app/Controllers/File/FileGetAction.php
new file mode 100644
index 0000000..a554fe0
--- /dev/null
+++ b/app/Controllers/File/FileGetAction.php
@@ -0,0 +1,18 @@
+model = $model;
+ }
+}
diff --git a/app/Controllers/File/FileLoadAction.php b/app/Controllers/File/FileLoadAction.php
new file mode 100644
index 0000000..6a898cf
--- /dev/null
+++ b/app/Controllers/File/FileLoadAction.php
@@ -0,0 +1,59 @@
+getAttribute('response_body');
+
+ // Load all models for the given client_id
+ $file = $this->file->clone();
+ $documents = $file
+ ->where('ResidentId', '=', $args['client_id'])
+ ->orderBy('Updated', 'desc')
+ ->get(['Id', 'ResidentId', 'FileName', 'Description', 'MediaType', 'Size', 'Created', 'Updated']);
+
+ // If the record is not found then 404 error, otherwise status is 200.
+ if ($documents !== null && count($documents) > 0) {
+ $data = $documents->toArray();
+ $status = ResponseCodes::HTTP_OK;
+ } else {
+ $data = null;
+ $status = ResponseCodes::HTTP_NOT_FOUND;
+ }
+
+ // Set the status and data of the ResponseBody
+ $responseBody = $responseBody
+ ->setData($data)
+ ->setStatus($status);
+
+ // Return the response as JSON
+ return $responseBody();
+ }
+}
diff --git a/app/Controllers/File/FileUpdateAction.php b/app/Controllers/File/FileUpdateAction.php
new file mode 100644
index 0000000..a372dac
--- /dev/null
+++ b/app/Controllers/File/FileUpdateAction.php
@@ -0,0 +1,18 @@
+model = $model;
+ }
+}
diff --git a/app/Controllers/File/FileUploadAction.php b/app/Controllers/File/FileUploadAction.php
new file mode 100644
index 0000000..8cacb4f
--- /dev/null
+++ b/app/Controllers/File/FileUploadAction.php
@@ -0,0 +1,65 @@
+getAttribute('response_body');
+
+ $parsedRequest = $responseBody->getParsedRequest();
+
+ /**
+ * @var $files UploadedFileInterface[]
+ * @phpstan-ignore-next-line
+ */
+ $files = $parsedRequest['uploaded_files'];
+
+ $file = $files['single_file'];
+ $clientId = $parsedRequest['client_id'];
+
+ /** @var File|FileRepresentation $document */
+ $document = clone $this->file;
+ $document->ResidentId = $clientId;
+ $document->Size = $file->getSize();
+ $document->FileName = $file->getClientFilename() ?? 'unknown';
+ $document->MediaType = $file->getClientMediaType();
+ $document->Image = $file->getStream()->getContents();
+ if ($document->save()) {
+ $responseBody = $responseBody->setData(
+ [
+ 'Id' => $document->Id,
+ 'Size' => $document->Size,
+ 'FileName' => $document->FileName,
+ 'Type' => $document->MediaType
+ ]
+ )->setStatus(ResponseCodes::HTTP_OK);
+ return $responseBody();
+ }
+
+ $responseBody = $responseBody
+ ->setData(null)
+ ->setStatus(ResponseCodes::HTTP_INTERNAL_SERVER_ERROR)
+ ->setMessage('Unable to save file');
+ return $responseBody();
+ }
+}
diff --git a/app/Controllers/File/FileUploadValidator.php b/app/Controllers/File/FileUploadValidator.php
new file mode 100644
index 0000000..868531a
--- /dev/null
+++ b/app/Controllers/File/FileUploadValidator.php
@@ -0,0 +1,70 @@
+getAttribute('response_body');
+ $parsedRequest = $responseBody->getParsedRequest();
+
+ /**
+ * @var $files UploadedFileInterface[]
+ * @phpstan-ignore-next-line
+ */
+ $files = $parsedRequest['uploaded_files'] ?? [];
+
+ // Only 1 file allowed to be uploaded
+ if (count($files) !== 1) {
+ $responseBody->registerParam('required', 'uploaded_files', 'array', 'There must be only one uploaded file');
+ }
+
+ // File is hard coded as 'single_file'
+ $file = $files['single_file'] ?? null;
+
+ // The single_file array element label is required
+ if ($file === null) {
+ $responseBody->registerParam('required', 'single_file', 'file', 'File must be labeled as single_file');
+ }
+
+ // File size must be under 100MB
+ if ($file && $file->getSize() > 104_857_600) {
+ $responseBody->registerParam('invalid', 'single_file', 'file', 'File size exceeds maximum allowed');
+ }
+
+ // client_id is required and must be an integer
+ $clientId = $parsedRequest['client_id'] ?? null;
+ if (!V::notEmpty()->validate($clientId)) {
+ $responseBody->registerParam('required', 'client_id', 'integer', 'client_id is empty or invalid');
+ } else {
+ if (!V::intVal()->validate($clientId)) {
+ $responseBody->registerParam('invalid', 'client_id', 'integer', 'client_id must be an integer');
+ }
+ }
+
+ // If there are any invalid or missing required then send bad request response
+ if ($responseBody->hasMissingRequiredOrInvalid()) {
+ $responseBody = $responseBody->setData(null)->setStatus(ResponseCodes::HTTP_BAD_REQUEST);
+ return $responseBody();
+ }
+
+ return $handler->handle($request);
+ }
+}
diff --git a/app/Controllers/Pillbox/PillboxLogAction.php b/app/Controllers/Pillbox/PillboxLogAction.php
index 62e35e7..fd97dc8 100644
--- a/app/Controllers/Pillbox/PillboxLogAction.php
+++ b/app/Controllers/Pillbox/PillboxLogAction.php
@@ -10,8 +10,10 @@
use Willow\Middleware\ResponseBody;
use Willow\Middleware\ResponseCodes;
use Willow\Models\MedHistory;
+use Willow\Models\MedHistoryRepresentation;
use Willow\Models\Medicine;
use Willow\Models\PillboxItem;
+use Willow\Models\PillboxItemRepresentation;
class PillboxLogAction
{
@@ -36,7 +38,7 @@ public function __invoke(Request $request, Response $response): ResponseInterfac
$pillboxId = $body['pillbox_id']; // Pillbox PK
/**
- * @var PillboxItem[] $pillboxItems
+ * @var PillboxItem[]|PillboxItemRepresentation[] $pillboxItems
* Get a collection of PillboxItems for the given pillboxId
*/
$pillboxItems = $this->pillboxItem->where('PillboxId', '=', $pillboxId)->get();
@@ -54,6 +56,9 @@ public function __invoke(Request $request, Response $response): ResponseInterfac
$medicineModel = $this->medicine->find($medicineId);
// We only log active medications
if ($medicineModel && $medicineModel->Active) {
+ /**
+ * @var MedHistoryRepresentation|MedHistory $medHistoryModel
+ */
$medHistoryModel = clone $this->medHistory;
$medHistoryModel->PillboxItemId = $id;
$medHistoryModel->ResidentId = $pillboxItem->ResidentId;
diff --git a/app/Controllers/Pin/PinAuthenticateValidator.php b/app/Controllers/Pin/PinAuthenticateValidator.php
index f816995..3425974 100644
--- a/app/Controllers/Pin/PinAuthenticateValidator.php
+++ b/app/Controllers/Pin/PinAuthenticateValidator.php
@@ -13,7 +13,7 @@
class PinAuthenticateValidator
{
- private const ALLOWED = ['pin_value'];
+ private const ALLOWED = ['pin_value', 'uploaded_files'];
/**
* @throws JsonException
diff --git a/app/Controllers/Pin/PinGenerateAction.php b/app/Controllers/Pin/PinGenerateAction.php
index a1186eb..0834e01 100644
--- a/app/Controllers/Pin/PinGenerateAction.php
+++ b/app/Controllers/Pin/PinGenerateAction.php
@@ -11,6 +11,7 @@
use Willow\Middleware\ResponseBody;
use Willow\Middleware\ResponseCodes;
use Willow\Models\Pin;
+use Willow\Models\PinRepresentation;
class PinGenerateAction
{
@@ -38,9 +39,10 @@ public function __invoke(Request $request, Response $response, array $args): Res
$parsedRequest = $responseBody->getParsedRequest();
$clientId = $parsedRequest['client_id'];
+ /** @var Pin|PinRepresentation $pinModel */
$pinModel = clone $this->pin;
do {
- $pinValue = random_int(10 ** (self::DIGIT_COUNT - 1), (10 ** self::DIGIT_COUNT) -1);
+ $pinValue = (string)random_int(10 ** (self::DIGIT_COUNT - 1), (10 ** self::DIGIT_COUNT) -1);
$pinExists = $this->pin
->where('ResidentId', '=', $clientId)
->where('PinValue', '=', $pinValue)
diff --git a/app/Controllers/Resident/ClientLoadAction.php b/app/Controllers/Resident/ClientLoadAction.php
index 0ab324d..98788b9 100644
--- a/app/Controllers/Resident/ClientLoadAction.php
+++ b/app/Controllers/Resident/ClientLoadAction.php
@@ -10,6 +10,7 @@
use Slim\Psr7\Response;
use Willow\Middleware\ResponseBody;
use Willow\Middleware\ResponseCodes;
+use Willow\Models\File;
use Willow\Models\MedHistory;
use Willow\Models\Medicine;
use Willow\Models\Pillbox;
@@ -19,11 +20,12 @@
class ClientLoadAction
{
public function __construct(
- private Resident $client,
- private Medicine $medicine,
- private MedHistory $medHistory,
- private Pillbox $pillbox,
- private PillboxItem $pillboxItem
+ private File $file,
+ private MedHistory $medHistory,
+ private Medicine $medicine,
+ private Pillbox $pillbox,
+ private PillboxItem $pillboxItem,
+ private Resident $client
) {
}
@@ -49,6 +51,11 @@ public function __invoke(Request $request, Response $response, array $args): Res
$status = ResponseCodes::HTTP_OK;
$data = [
'clientInfo' => $client->attributesToArray(),
+ 'fileList' => $this->file
+ ->where('ResidentId', '=', $clientId)
+ ->orderBy('Updated', 'desc')
+ ->get(['Id', 'ResidentId', 'FileName', 'Description', 'MediaType', 'Size', 'Created', 'Updated'])
+ ->toArray(),
'medicineList' => $this->medicine
->where('ResidentId', '=', $clientId)
->orderBy('Drug', 'asc')
diff --git a/app/Controllers/SearchValidatorBase.php b/app/Controllers/SearchValidatorBase.php
index 81ee733..7f59626 100644
--- a/app/Controllers/SearchValidatorBase.php
+++ b/app/Controllers/SearchValidatorBase.php
@@ -15,6 +15,7 @@ class SearchValidatorBase extends ActionBase
private const ALLOWED_PARAMETER_KEYS = [
'api_key',
'id',
+ 'uploaded_files',
'crossJoin',
'distinct',
'first',
diff --git a/app/Middleware/RegisterRouteControllers.php b/app/Middleware/RegisterRouteControllers.php
index 0bda509..3226102 100644
--- a/app/Middleware/RegisterRouteControllers.php
+++ b/app/Middleware/RegisterRouteControllers.php
@@ -11,29 +11,32 @@
use Willow\Controllers\PillboxItem\PillboxItemController;
use Willow\Controllers\Pin\PinController;
use Willow\Controllers\Resident\ResidentController;
+use Willow\Controllers\File\FileController;
class RegisterRouteControllers
{
public function __construct(
private AuthenticateController $authenticateController,
- private MedHistoryController $medHistoryController,
- private MedicineController $medicineController,
- private PillboxController $pillboxController,
- private PillboxItemController $pillboxItemController,
- private ResidentController $residentController,
- private PinController $pinController
+ private FileController $fileController,
+ private MedHistoryController $medHistoryController,
+ private MedicineController $medicineController,
+ private PillboxController $pillboxController,
+ private PillboxItemController $pillboxItemController,
+ private PinController $pinController,
+ private ResidentController $residentController
) {
}
public function __invoke(RouteCollectorProxy $collectorProxy): self {
// Register routes and actions for each controller
$this->authenticateController->register($collectorProxy);
+ $this->fileController->register($collectorProxy);
$this->medHistoryController->register($collectorProxy);
$this->medicineController->register($collectorProxy);
$this->pillboxController->register($collectorProxy);
$this->pillboxItemController->register($collectorProxy);
- $this->residentController->register($collectorProxy);
$this->pinController->register($collectorProxy);
+ $this->residentController->register($collectorProxy);
return $this;
}
}
diff --git a/app/Middleware/ResponseBodyFactory.php b/app/Middleware/ResponseBodyFactory.php
index 2ffa31e..8d2a812 100644
--- a/app/Middleware/ResponseBodyFactory.php
+++ b/app/Middleware/ResponseBodyFactory.php
@@ -28,7 +28,8 @@ public function __invoke(Request $request, RequestHandler $handler): ResponseInt
array_merge(
$arguments,
$request->getQueryParams(),
- $request->getParsedBody() ?? []
+ $request->getParsedBody() ?? [],
+ ['uploaded_files' => $request->getUploadedFiles()]
)
)
)
diff --git a/app/Models/File.php b/app/Models/File.php
new file mode 100644
index 0000000..93146e8
--- /dev/null
+++ b/app/Models/File.php
@@ -0,0 +1,51 @@
+attributes['Description'] = null;
+ } else {
+ $this->attributes['Description'] = $value;
+ }
+ }
+}
+
diff --git a/app/Models/FileRepresentation.php b/app/Models/FileRepresentation.php
new file mode 100644
index 0000000..03416fe
--- /dev/null
+++ b/app/Models/FileRepresentation.php
@@ -0,0 +1,21 @@
+attributes['Drug'] = trim($value);
}
+
/**
* Override Strength field to null if empty string
* @param string|null $value
diff --git a/app/Models/ModelBase.php b/app/Models/ModelBase.php
index a361950..d8ce739 100644
--- a/app/Models/ModelBase.php
+++ b/app/Models/ModelBase.php
@@ -27,7 +27,7 @@ protected static function booted(): void {
static::addGlobalScope(new UserScope());
// Save the authenticated UserId value to the model
- static::saving(function ($model) {
+ static::saving(static function ($model) {
$model->UserId = UserScope::getUserId();
});
}
diff --git a/app/Models/ModelDefaultRules.php b/app/Models/ModelDefaultRules.php
index 69512a7..ba056dd 100644
--- a/app/Models/ModelDefaultRules.php
+++ b/app/Models/ModelDefaultRules.php
@@ -10,7 +10,7 @@ class ModelDefaultRules
/**
* Allowed request parameters that are not column names
*/
- private const WHITE_LIST = ['id', 'api_key'];
+ private const WHITE_LIST = ['id', 'api_key', 'uploaded_files'];
/**
* @param ResponseBody $responseBody
diff --git a/app/Models/PillboxItemRepresentation.php b/app/Models/PillboxItemRepresentation.php
new file mode 100644
index 0000000..f906c93
--- /dev/null
+++ b/app/Models/PillboxItemRepresentation.php
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
diff --git a/phpstan.neon b/phpstan.neon
index 7c1ebcb..8ec765d 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -1,23 +1 @@
-parameters:
- ignoreErrors:
- -
- message: '#Access to an undefined property [a-zA-Z0-9\\_]+::\$Quantity#'
- path: app/Controllers/Pillbox/PillboxLogAction.php
- -
- message: '#Access to an undefined property [a-zA-Z0-9\\_]+::\$MedicineId#'
- path: app/Controllers/Pillbox/PillboxLogAction.php
- -
- message: '#Access to an undefined property [a-zA-Z0-9\\_]+::\$Id#'
- path: app/Controllers/Pillbox/PillboxLogAction.php
- -
- message: '#Access to an undefined property [a-zA-Z0-9\\_]+::\$PillboxItemId#'
- path: app/Controllers/Pillbox/PillboxLogAction.php
- -
- message: '#Access to an undefined property [a-zA-Z0-9\\_]+::\$ResidentId#'
- path: app/Controllers/Pillbox/PillboxLogAction.php
- -
- message: '#Access to an undefined property [a-zA-Z0-9\\_]+::\$Notes#'
- path: app/Controllers/Pillbox/PillboxLogAction.php
-
- universalObjectCratesClasses:
- - Willow\Models\Pin
+# phpstan configuration file
diff --git a/sql/File.sql b/sql/File.sql
new file mode 100644
index 0000000..38eeea1
--- /dev/null
+++ b/sql/File.sql
@@ -0,0 +1,18 @@
+CREATE TABLE `File` (
+ `Id` int NOT NULL AUTO_INCREMENT,
+ `ResidentId` int NOT NULL,
+ `UserId` int NOT NULL,
+ `FileName` varchar(65) NOT NULL,
+ `MediaType` varchar(65) DEFAULT NULL,
+ `Size` int DEFAULT NULL,
+ `Description` varchar(100) DEFAULT NULL,
+ `Image` longblob,
+ `Created` timestamp NULL DEFAULT NULL,
+ `Updated` timestamp NULL DEFAULT NULL,
+ `deleted_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`Id`),
+ KEY `fk_File_User_idx` (`UserId`),
+ KEY `fk_File_Resident_idx` (`ResidentId`),
+ CONSTRAINT `fk_File_Resident` FOREIGN KEY (`ResidentId`) REFERENCES `Resident` (`Id`),
+ CONSTRAINT `fk_File_User` FOREIGN KEY (`UserId`) REFERENCES `User` (`Id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
diff --git a/sql/Pin.sql b/sql/Pin.sql
deleted file mode 100644
index a3719cf..0000000
--- a/sql/Pin.sql
+++ /dev/null
@@ -1,16 +0,0 @@
-CREATE TABLE `Pin` (
- `Id` int NOT NULL AUTO_INCREMENT,
- `ResidentId` int NOT NULL,
- `UserId` int NOT NULL,
- `PinValue` char(6) NOT NULL,
- `Image` longblob,
- `Created` timestamp NULL DEFAULT NULL,
- `Updated` timestamp NULL DEFAULT NULL,
- `deleted_at` timestamp NULL DEFAULT NULL,
- PRIMARY KEY (`Id`),
- UNIQUE KEY `unique_pin` (`ResidentId`,`UserId`,`PinValue`),
- KEY `fk_Pin_User` (`UserId`),
- KEY `fk_Pin_Resident` (`ResidentId`),
- CONSTRAINT `fk_Pin_Resident` FOREIGN KEY (`ResidentId`) REFERENCES `Resident` (`Id`),
- CONSTRAINT `fk_Pin_User` FOREIGN KEY (`UserId`) REFERENCES `User` (`Id`)
-) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;