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:
Jepp9350
2025-03-10 13:11:44 +01:00
parent fdb536ea3e
commit 48ad281baa
6 changed files with 519 additions and 1 deletions
@@ -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'
]
);
}
}
+4 -1
View File
@@ -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);
}
/**
+41
View File
@@ -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