Add notifications module with routes, types, and helpers
Introduced a complete notifications module, including classes for managing notifications (`notifications_o`), traits for handling types and routing, and API routes to list, add, and delete notifications. Added input validation, permission handling, and JSON data processing capabilities.
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
trait notification_type_t
|
||||
{
|
||||
/**
|
||||
* The name of the notification type
|
||||
* @var string
|
||||
*/
|
||||
public string $name;
|
||||
/**
|
||||
* The description of the notification type
|
||||
* @var string
|
||||
*/
|
||||
public string $description;
|
||||
/**
|
||||
* The type of the notification
|
||||
* @notation This is the identifier of the notification type, it is used to identify the notification type in the system.
|
||||
* @var string
|
||||
*/
|
||||
public string $type;
|
||||
|
||||
|
||||
/**
|
||||
* The constructor of the notification type
|
||||
* @throws Exception If the id is not set
|
||||
* @throws Exception If the name is not set
|
||||
* @throws Exception If the description is not set
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
self::run();
|
||||
self::validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* The run method, to be implemented by the class using this trait
|
||||
* @notation This method should be implemented in the class using this trait, it is not meant to be called directly. It will be called when the notification type is triggered.
|
||||
* @throws Exception If type is not set
|
||||
* @throws Exception If name is not set
|
||||
* @throws Exception If description is not set
|
||||
*/
|
||||
abstract public function run(): void;
|
||||
|
||||
/**
|
||||
* Validate the notification type
|
||||
* @throws Exception If the type is not set
|
||||
* @throws Exception If the name is not set
|
||||
* @throws Exception If the description is not set
|
||||
*/
|
||||
public function validate(): void
|
||||
{
|
||||
if (!isset($this->type)) {
|
||||
throw new Exception('The type is not set');
|
||||
}
|
||||
if (empty($this->name)) {
|
||||
throw new Exception('The name is not set');
|
||||
}
|
||||
if (empty($this->description)) {
|
||||
throw new Exception('The description is not set');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the type of the notification
|
||||
* @param string $type
|
||||
* @throws Exception If the type is empty
|
||||
* @throws Exception If the type is not a string
|
||||
* @throws Exception If the type is not a valid format (e.g. "TYPE_NAME")
|
||||
*/
|
||||
public function set_type(string $type): void
|
||||
{
|
||||
if (empty($type)) {
|
||||
throw new Exception('The type is empty');
|
||||
}
|
||||
if (!preg_match('/^[A-Z_]+$/', $type)) {
|
||||
throw new Exception('The type is not a valid format (e.g. "TYPE_NAME")');
|
||||
}
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the name of the notification type
|
||||
* @param string $name
|
||||
* @throws Exception If the name is empty
|
||||
*/
|
||||
public function set_name(string $name): void
|
||||
{
|
||||
if (empty($name)) {
|
||||
throw new Exception('The name is empty');
|
||||
}
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the description of the notification type
|
||||
* @param string $description
|
||||
* @throws Exception If the description is empty
|
||||
*/
|
||||
public function set_description(string $description): void
|
||||
{
|
||||
if (empty($description)) {
|
||||
throw new Exception('The description is empty');
|
||||
}
|
||||
$this->description = $description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification type as an array
|
||||
* @return array
|
||||
*/
|
||||
public function as_array(): array
|
||||
{
|
||||
return [
|
||||
'type' => $this->type,
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'data' => self::get_data(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data of the notification type
|
||||
* @notation This takes all data_* properties and returns them as an array
|
||||
* @return array
|
||||
*/
|
||||
public function get_data(): array
|
||||
{
|
||||
// Get all the properties of the class
|
||||
$properties = get_object_vars($this);
|
||||
// Filter the properties to only include those that start with "data_"
|
||||
$data_properties = array_filter($properties, function ($key) {
|
||||
return str_starts_with($key, 'data_');
|
||||
}, ARRAY_FILTER_USE_KEY);
|
||||
// Remove the "data_" prefix from the keys
|
||||
// Return the data properties as an array
|
||||
return array_combine(
|
||||
array_map(function ($key) {
|
||||
return substr($key, 5);
|
||||
}, array_keys($data_properties)),
|
||||
array_values($data_properties)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification type as an object
|
||||
* @return object
|
||||
*/
|
||||
public function as_object(): object
|
||||
{
|
||||
return (object)[
|
||||
'type' => $this->type,
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'data' => self::get_data(),
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace types;
|
||||
|
||||
use notification_type_t;
|
||||
|
||||
class notifications_type_new_booking_c
|
||||
{
|
||||
use notification_type_t;
|
||||
|
||||
/**
|
||||
* The id of the booking
|
||||
* @var int $data_booking_id The id of the booking
|
||||
*/
|
||||
public int $data_booking_id;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
self::set_type('NEW_BOOKING');
|
||||
self::set_description('This notification is sent when a new booking is created');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace objects;
|
||||
|
||||
use classes\db;
|
||||
use classes\object_property;
|
||||
use Exception;
|
||||
use traits\db_object_t;
|
||||
|
||||
class notifications_o extends db
|
||||
{
|
||||
use db_object_t;
|
||||
|
||||
/**
|
||||
* The notification type
|
||||
* @var object_property $type The notification type
|
||||
*/
|
||||
public object_property $type;
|
||||
/**
|
||||
* The user id
|
||||
* @var object_property $user_id The user id
|
||||
*/
|
||||
public object_property $user_id;
|
||||
/**
|
||||
* The notification data
|
||||
* @var object_property $data The notification data (JSON encoded)
|
||||
*/
|
||||
public object_property $data;
|
||||
/**
|
||||
* The notification created at
|
||||
* @var object_property $created_at The notification created at
|
||||
*/
|
||||
public object_property $created_at;
|
||||
|
||||
|
||||
public function structure(): void
|
||||
{
|
||||
$this->setTable('notifications');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a notification object, and set this object to the new object
|
||||
* @param string $type The notification type id
|
||||
* @param int $user_id The user id
|
||||
* @param array $data The notification data
|
||||
* @return void
|
||||
* @throws Exception If the object was not created successfully
|
||||
*/
|
||||
public function add(string $type, int $user_id, array $data): void
|
||||
{
|
||||
global /** @var db $db */
|
||||
$db;
|
||||
// Sanitize the input
|
||||
$type = $db->escape_string($type);
|
||||
$user_id = (int)$user_id;
|
||||
// JSON encode the data
|
||||
$encoded_data = $db->escape_string(json_encode($data));
|
||||
// Add the object
|
||||
$tmp_id = self::add_object([
|
||||
'type' => $type,
|
||||
'user_id' => $user_id,
|
||||
'data' => $encoded_data,
|
||||
]);
|
||||
if (!$tmp_id) {
|
||||
throw new Exception('The object was not created successfully.');
|
||||
}
|
||||
$this->id = $tmp_id;
|
||||
self::getObjectProperties();
|
||||
self::objectChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the object properties
|
||||
* @return void
|
||||
*/
|
||||
public function getObjectProperties(): void
|
||||
{
|
||||
$this->type = new object_property($this->table, $this->id, 'type', 'string', false);
|
||||
$this->user_id = new object_property($this->table, $this->id, 'user_id', 'int', false);
|
||||
$this->data = new object_property($this->table, $this->id, 'data', 'string', false);
|
||||
$this->created_at = new object_property($this->table, $this->id, 'created_at', 'datetime', false);
|
||||
}
|
||||
|
||||
public function objectChanged(): void
|
||||
{
|
||||
//TODO: Add cache invalidation
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the object as an array
|
||||
* @return array The object as an array
|
||||
*/
|
||||
public function asArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => (int)$this->id,
|
||||
'type' => (string)$this->type->value(),
|
||||
'user_id' => (int)$this->user_id->value(),
|
||||
'data' => self::decodeData((string)$this->data->value()),
|
||||
'created_at' => (string)$this->created_at->value(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the data
|
||||
* @param string $data The data JSON encoded
|
||||
* @return array The data
|
||||
*/
|
||||
public static function decodeData(string $data): array
|
||||
{
|
||||
return json_decode($data, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
namespace routes;
|
||||
|
||||
use classes\authentication;
|
||||
use objects\logs_o;
|
||||
use objects\notifications_o;
|
||||
use traits\route_t;
|
||||
|
||||
class notificationsRoute
|
||||
{
|
||||
use route_t;
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$this->get('/notifications', function () {
|
||||
// Require the user to be logged in
|
||||
global $response;
|
||||
$this->requirePermission('list_notifications');
|
||||
// Check if the user has permission to list all notifications
|
||||
if (self::hasPermission('list_all_notifications')) {
|
||||
$this->requirePermission('list_all_notifications');
|
||||
} else {
|
||||
$this->requirePermission('list_own_notifications');
|
||||
}
|
||||
// Get the user object
|
||||
$user = (new authentication())->get_user();
|
||||
// Check if the request was successful
|
||||
if ($user) {
|
||||
// Log the incident
|
||||
(new logs_o())->add('notifications', 'global', 1, $user->id, 'LIST_OWN_NOTIFICATIONS', 'User accessed the list of notifications');
|
||||
$notifications = new notifications_o();
|
||||
// Return the list of notifications
|
||||
$response->success(
|
||||
$notifications
|
||||
->setSearchableFields([
|
||||
// The fields that can be searched. This would otherwise make it possible to get secret information from the database, simply by searching for it and getting the result count back
|
||||
'id',
|
||||
'type',
|
||||
'user_id',
|
||||
'data',
|
||||
'created_at',
|
||||
'deleted_at',
|
||||
])
|
||||
->listObjectsWithPaginationIfSet(
|
||||
function ($notification) use ($notifications, $user) {
|
||||
$tmp_notification = [
|
||||
'id' => (int)$notification['id'],
|
||||
'type' => (string)$notification['type'],
|
||||
'user_id' => (int)$notification['user_id'],
|
||||
'data' => $notification['data'] ? $notifications->decodeData($notification['data']) : null,
|
||||
'created_at' => (string)$notification['created_at'],
|
||||
'deleted_at' => $notification['deleted_at'] ? (string)$notification['deleted_at'] : null,
|
||||
];
|
||||
return $tmp_notification;
|
||||
},
|
||||
$notifications->forceRestrictFilters(
|
||||
[
|
||||
// This makes sure that the user can only see their own notifications
|
||||
'user_id' => $user->id,
|
||||
]
|
||||
)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// Log the incident
|
||||
(new logs_o())->add('notifications', 'global', 1, 0, 'LIST_OWN_NOTIFICATIONS', 'No user found, or invalid session');
|
||||
// Return an error
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
},
|
||||
[
|
||||
'list_notifications' => 'List notifications, provided the user has either list_all_notifications, or list_own_notifications permission',
|
||||
'list_own_notifications' => 'List all notifications for the logged in user',
|
||||
'list_all_notifications' => 'List all notifications for all users (superuser only)',
|
||||
]
|
||||
);
|
||||
|
||||
$this->post('/notifications', function () {
|
||||
// Require the user to be logged in
|
||||
global $response;
|
||||
self::requirePermission('add_notification');
|
||||
// Get the user object
|
||||
$user = (new authentication())->get_user();
|
||||
// Check if the request was successful
|
||||
if ($user) {
|
||||
// Get the post data
|
||||
$data = [];
|
||||
// Check if the required fields are set
|
||||
self::requireParameters(['type', 'user_id']);
|
||||
self::requireType((string)self::getParameter('type'), self::TYPE_STRING());
|
||||
self::requireType((int)self::getParameter('user_id'), self::TYPE_INT());
|
||||
// If the data is set, check if it is an array
|
||||
if (self::isParametersSet(['data'])) {
|
||||
self::requireTypeIn(
|
||||
(array)self::getParameter('data'),
|
||||
[
|
||||
self::TYPE_ARRAY(),
|
||||
self::TYPE_NULL()
|
||||
]
|
||||
);
|
||||
// Check if the data is an array
|
||||
if (self::getParameter('data') !== null) {
|
||||
// JSON decode the data
|
||||
$data = json_decode(self::getParameter('data'), true);
|
||||
}
|
||||
}
|
||||
// Check if the user_id is set
|
||||
// Add the notification
|
||||
(new notifications_o())->add(
|
||||
(string)self::getParameter('type'),
|
||||
(int)self::getParameter('user_id'),
|
||||
(array)$data
|
||||
);
|
||||
// Log the incident
|
||||
(new logs_o())->add('notifications', 'global', 1, $user->id, 'ADD_NOTIFICATION', 'User added a notification');
|
||||
// Return the list of departments
|
||||
$response->success(['message' => 'Notification added successfully']);
|
||||
} else {
|
||||
// Log the incident
|
||||
(new logs_o())->add('notifications', 'global', 1, 0, 'ADD_NOTIFICATION', 'No user found, or invalid session');
|
||||
// Return an error
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
},
|
||||
[
|
||||
'add_notification' => 'Add a notification'
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
$this->delete('/notifications', function () {
|
||||
// Require the user to be logged in
|
||||
global $response;
|
||||
self::requirePermission('delete_own_notifications');
|
||||
// Get the user object
|
||||
$user = (new authentication())->get_user();
|
||||
// Check if the request was successful
|
||||
if ($user) {
|
||||
// Require the parameters
|
||||
self::requireParameters(['id']);
|
||||
self::requireType((int)self::getParameter('id'), self::TYPE_INT());
|
||||
// Get the department category object
|
||||
$notification = (new notifications_o())->select(self::getParameter('id'));
|
||||
// Validate the department category object
|
||||
if (!$notification->exists()) {
|
||||
// Log the incident
|
||||
(new logs_o())->add('notifications', 'global', 1, $user->id, 'DELETE_OWN_NOTIFICATIONS', 'User tried to delete a notification that does not exist');
|
||||
// Return an error
|
||||
$response->error('Notification does not exist', 400);
|
||||
}
|
||||
// Check if the user is the owner of the notification
|
||||
if ((int)$notification->user_id->value() !== (int)$user->id) {
|
||||
// Log the incident
|
||||
(new logs_o())->add('notifications', 'global', 1, $user->id, 'DELETE_OWN_NOTIFICATIONS', 'User tried to delete a notification that does not belong to them');
|
||||
// Return an error
|
||||
$response->error('You do not have permission to delete this notification', 403);
|
||||
}
|
||||
// Delete the department category
|
||||
$notification->delete();
|
||||
// Log the incident
|
||||
(new logs_o())->add('notifications', 'global', 1, $user->id, 'DELETE_OWN_NOTIFICATIONS', 'User deleted a notification');
|
||||
// Return the list of departments
|
||||
$response->success(['message' => 'Notification deleted successfully']);
|
||||
} else {
|
||||
// Log the incident
|
||||
(new logs_o())->add('notifications', 'global', 1, 0, 'DELETE_OWN_NOTIFICATIONS', 'No user found, or invalid session');
|
||||
// Return an error
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
},
|
||||
[
|
||||
'delete_own_notifications' => 'Delete a notification'
|
||||
]
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -195,12 +195,15 @@ trait db_object_t
|
||||
if (!in_array($order[array_key_first($order)], ['ASC', 'DESC'])) {
|
||||
throw new Exception('Invalid order direction, must be ASC or DESC');
|
||||
}
|
||||
} else {
|
||||
$order = ['id' => 'ASC']; // Default order
|
||||
}
|
||||
// List the objects with pagination
|
||||
if ($page && $limit) {
|
||||
return $this->listObjectsWithPagination($page, $limit, $search, $filters, $order, $parseFunction);
|
||||
}
|
||||
return $this->listObjects($parseFunction);
|
||||
// If the page and limit are not set, list all objects
|
||||
return $this->listObjectsWithPagination(1, 1000, $search, $filters, $order, $parseFunction);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,6 +31,15 @@ trait route_t
|
||||
{
|
||||
// Get the type of the value
|
||||
$value_type = gettype($value);
|
||||
// If the type is Array, or Object, json_decode the value to check if it is a valid JSON
|
||||
if ($type === 'array' || $type === 'object') {
|
||||
$value = json_decode($value, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
global $response;
|
||||
$response->error('Invalid JSON. Value: ' . $value, 400);
|
||||
}
|
||||
$value_type = gettype($value);
|
||||
}
|
||||
// Check if the value is of the required type
|
||||
if ($value_type !== $type) {
|
||||
global $response;
|
||||
@@ -57,6 +66,16 @@ trait route_t
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* TYPE_ARRAY
|
||||
* @note This is used to check if the value is an array, this is used in routes to validate input
|
||||
* @return string
|
||||
*/
|
||||
public function TYPE_ARRAY(): string
|
||||
{
|
||||
return 'array';
|
||||
}
|
||||
|
||||
/**
|
||||
* TYPE_NULL
|
||||
* @note This is used to check if the value is null, this is used in routes to validate input
|
||||
@@ -156,6 +175,28 @@ trait route_t
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has a permission
|
||||
* @param string $permission
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPermission(string $permission): bool
|
||||
{
|
||||
global $response;
|
||||
// Check if the users authorization token has the required permission
|
||||
try {
|
||||
$user = (new authentication())->get_user();
|
||||
// If there is no user, return an error
|
||||
if (!$user) {
|
||||
(new logs_o())->add('global', 'global', 1, 0, 'AUTHENTICATION_FAILED', 'Authentication failed. Invalid, or missing token');
|
||||
$response->error('Authentication failed. Invalid or missing token.', 401);
|
||||
}
|
||||
return $user->hasPermission($permission);
|
||||
} catch (\Exception $e) {
|
||||
$response->error($e->getMessage(), 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Require parameter to be a positive integer
|
||||
* @param int $value The value to check
|
||||
|
||||
Reference in New Issue
Block a user