License updated to GPLv3

🧲 New features
Custom user role permissions
Employee edit form updated
Employee daily task list
Attendance and employee distribution charts on dashboard
Improvements to company structure and company assets module
Improved tables for displaying data in several modules
Faster data loading (specially for employee module)
Initials based profile pictures
Re-designed login page
Re-designed user profile page
Improvements to filtering
New REST endpoints for employee qualifications

🐛 Bug fixes
Fixed, issue with managers being able to create performance reviews for employees who are not their direct reports
Fixed, issues related to using full profile image instead of using smaller version of profile image
Changing third gender to other
Improvements and fixes for internal frontend data caching
This commit is contained in:
Thilina Pituwala
2020-10-31 19:02:37 +01:00
parent 86b8345505
commit b1df0037db
29343 changed files with 867614 additions and 2191082 deletions

View File

@@ -22,6 +22,10 @@ abstract class AbstractModuleManager
private $moduleType = null;
private $actionManager = null;
public function initialize()
{
}
/**
* Override this method in module manager class to define user classes.
* A user class is a class that is mapped to a table having a field named profile.

View File

@@ -12,6 +12,7 @@ namespace Classes;
use Classes\Crypt\AesCtr;
use Classes\Email\EmailSender;
use Classes\Exception\IceHttpException;
use Company\Common\Model\CompanyStructure;
use Employees\Common\Model\Employee;
use Employees\Common\Model\EmployeeApproval;
@@ -23,6 +24,7 @@ use Model\Setting;
use Modules\Common\Model\Module;
use Permissions\Common\Model\Permission;
use Users\Common\Model\User;
use Users\Common\Model\UserRole;
use Utils\LogManager;
use Utils\SessionUtils;
@@ -155,6 +157,11 @@ class BaseService
return $list;
}
public function getModelClassMap()
{
return $this->modelClassMap;
}
public function addModelClass($modelClass, $fullQualifiedName)
{
$this->modelClassMap[$modelClass] = $fullQualifiedName;
@@ -245,7 +252,7 @@ class BaseService
$countFilterQuery = $response[0];
$countFilterQueryData = $response[1];
} else {
$defaultFilterResp = \Classes\BaseService::getInstance()->buildDefaultFilterQuery($filter);
$defaultFilterResp = BaseService::getInstance()->buildDefaultFilterQuery($filter);
$countFilterQuery = $defaultFilterResp[0];
$countFilterQueryData = $defaultFilterResp[1];
}
@@ -253,9 +260,9 @@ class BaseService
}
if (in_array($table, \Classes\BaseService::getInstance()->userTables)
if (in_array($table, BaseService::getInstance()->userTables)
&& !$skipProfileRestriction && !$isSubOrdinates) {
$cemp = \Classes\BaseService::getInstance()->getCurrentProfileId();
$cemp = BaseService::getInstance()->getCurrentProfileId();
$sql = "Select count(id) as count from "
. $obj->_table . " where " . SIGN_IN_ELEMENT_MAPPING_FIELD_NAME . " = ? " . $countFilterQuery;
array_unshift($countFilterQueryData, $cemp);
@@ -263,8 +270,8 @@ class BaseService
$rowCount = $obj->DB()->Execute($sql, $countFilterQueryData);
} else {
if ($isSubOrdinates) {
$cemp = \Classes\BaseService::getInstance()->getCurrentProfileId();
$profileClass = \Classes\BaseService::getInstance()->getFullQualifiedModelClassName(
$cemp = BaseService::getInstance()->getCurrentProfileId();
$profileClass = BaseService::getInstance()->getFullQualifiedModelClassName(
ucfirst(SIGN_IN_ELEMENT_MAPPING_FIELD_NAME)
);
$subordinate = new $profileClass();
@@ -1273,15 +1280,10 @@ class BaseService
public function checkSecureAccess($type, $object, $table, $request)
{
//Construct permission method
$permMethod = "get".str_replace(' ', '', $this->currentUser->user_level)."Access";
$userOnlyMeAccessRequestField = $object->getUserOnlyMeAccessRequestField();
$userOnlyMeAccessField = $object->getUserOnlyMeAccessField();
if (method_exists($object, $permMethod)) {
$accessMatrix = $object->$permMethod($this->currentUser->user_roles);
} else {
$accessMatrix = $object->getDefaultAccessLevel();
}
$accessMatrix = $object->getRoleBasedAccess($this->currentUser->user_level, $this->currentUser->user_roles);
if (in_array($type, $accessMatrix)) {
//The user has required permission, so return true
@@ -1314,15 +1316,11 @@ class BaseService
}
}
$ret['status'] = "ERROR";
$ret['message'] = $type." ".get_class($object)." Access violation";
echo json_encode($ret);
$exception = new \Exception(
sprintf(
'%s : %s',
'Access violation',
json_encode([$type, $table, get_class($object), $request, json_encode($this->currentUser)])
)
$action = PermissionManager::ACCESS_LIST_DESCRIPTION[$type];
$message = "You are not allowed to $action object type ".$object->table.'.';
$exception = new IceHttpException(
$message,
403
);
LogManager::getInstance()->notifyException($exception);
throw $exception;
@@ -1802,7 +1800,7 @@ END;
$companyStructure->Load('id = ?', array($parentCompanyStructure));
}
} while (!empty($companyStructure->id)
&& !empty($parentCompanyStructure)
&& !empty($parentCompanyStructure)
);
}
@@ -1938,4 +1936,21 @@ END;
{
return defined('QUERY_CACHE') && QUERY_CACHE === true;
}
public function getCurrentDBUser()
{
$user = BaseService::getInstance()->getCurrentUser();
if (empty($user)) {
return new IceResponse(IceResponse::ERROR);
}
$dbUser = new User();
$dbUser->Load("id = ?", array($user->id));
return $dbUser;
}
public function getAccessToken()
{
$dbUser = $this->getCurrentDBUser();
return RestApiManager::getInstance()->getAccessTokenForUser($dbUser);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Classes\Common;
use Pimple\Container;
class IceContainer extends Container
{
public function get($serviceName)
{
return $this[$serviceName];
}
public function set($serviceName, $callback)
{
$this[$serviceName] = $callback;
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Classes\Exception;
class IceHttpException extends \Exception
{
}

View File

@@ -56,10 +56,10 @@ class FileService
}
}
public function checkAddSmallProfileImage($profileImage)
public function checkAddSmallProfileImageS3($profileImage)
{
$file = new File();
$file->Load('name = ?', array($profileImage->name."_small"));
$file->Load('file_group = ? and employee = ?', array('profile_image_small', $profileImage->employee));
if (empty($file->id)) {
LogManager::getInstance()->info("Small profile image ".$profileImage->name."_small not found");
@@ -70,7 +70,6 @@ class FileService
$signInMappingField = SIGN_IN_ELEMENT_MAPPING_FIELD_NAME;
$file->$signInMappingField = $profileImage->$signInMappingField;
$file->filename = $file->name.str_replace($profileImage->name, "", $profileImage->filename);
$file->file_group = $profileImage->file_group;
file_put_contents("/tmp/".$file->filename."_orig", file_get_contents($largeFileUrl));
@@ -78,7 +77,7 @@ class FileService
//Resize image to 100
$img = new \Classes\SimpleImage("/tmp/".$file->filename."_orig");
$img->fitToWidth(100);
$img->fitToWidth(140);
$img->save("/tmp/".$file->filename);
$uploadFilesToS3Key = SettingsManager::getInstance()->getSetting(
@@ -100,7 +99,18 @@ class FileService
LogManager::getInstance()->info("Upload Result:".print_r($result, true));
$file->employee = $profileImage->employee;
$file->file_group = 'profile_image_small';
$file->size = filesize(CLIENT_BASE_PATH.'data/'.$file->filename);
$file->size_text = $this->getReadableSize($file->size);
if (!empty($result)) {
$fileDelete = new File();
$fileDelete->Load('filename = ?', [$file->filename]);
if ($fileDelete->filename === $file->filename) {
$fileDelete->Delete();
}
$file->Save();
}
@@ -113,16 +123,56 @@ class FileService
return $file;
}
public function checkAddSmallProfileImage($profileImage)
{
$file = new File();
$file->Load('file_group = ? and employee = ?', array('profile_image_small', $profileImage->employee));
if (empty($file->id)) {
LogManager::getInstance()->info("Small profile image ".$profileImage->name."_small not found");
$largeFileUrl = $this->getFileUrl($profileImage->name);
file_put_contents("/tmp/".$profileImage->filename."_orig", file_get_contents($largeFileUrl));
if (file_exists("/tmp/".$profileImage->filename."_orig")) {
//Resize image to 100
$file->name = $profileImage->name."_small";
$signInMappingField = SIGN_IN_ELEMENT_MAPPING_FIELD_NAME;
$file->$signInMappingField = $profileImage->$signInMappingField;
$file->filename = $file->name.str_replace($profileImage->name, "", $profileImage->filename);
$img = new \Classes\SimpleImage("/tmp/".$profileImage->filename."_orig");
$img->fitToWidth(140);
$img->save(CLIENT_BASE_PATH.'data/'.$file->filename);
$file->employee = $profileImage->employee;
$file->file_group = 'profile_image_small';
$file->size = filesize(CLIENT_BASE_PATH.'data/'.$file->filename);
$file->size_text = $this->getReadableSize($file->size);
$file->Save();
unlink("/tmp/".$file->filename."_orig");
return $file;
}
return null;
}
return $file;
}
public function updateSmallProfileImage($profile)
{
$file = new File();
$file->Load('name = ?', array('profile_image_'.$profile->id));
$file->Load('file_group = ? and employee = ?', array('profile_image', $profile->id));
if ($file->name == 'profile_image_'.$profile->id) {
if ($file->employee == $profile->id) {
$uploadFilesToS3 = SettingsManager::getInstance()->getSetting("Files: Upload Files to S3");
if ($uploadFilesToS3 == "1") {
try {
$fileNew = $this->checkAddSmallProfileImage($file);
$fileNew = $this->checkAddSmallProfileImageS3($file);
if (!empty($fileNew)) {
$file = $fileNew;
}
@@ -147,23 +197,16 @@ class FileService
} catch (\Exception $e) {
LogManager::getInstance()->error("Error generating profile image: ".$e->getMessage());
LogManager::getInstance()->notifyException($e);
if ($profile->gender == 'Female') {
$profile->image = BASE_URL."images/user_female.png";
} else {
$profile->image = BASE_URL."images/user_male.png";
}
$profile->image = $this->generateProfileImage($profile->first_name, $profile->last_name);
}
} elseif (substr($file->filename, 0, 8) === 'https://') {
$profile->image = $file->filename;
} else {
$profile->image = CLIENT_BASE_URL.'data/'.$file->filename;
$fileNew = $this->checkAddSmallProfileImage($file);
$profile->image = CLIENT_BASE_URL.'data/'.$fileNew->filename;
}
} else {
if ($profile->gender == 'Female') {
$profile->image = BASE_URL."images/user_female.png";
} else {
$profile->image = BASE_URL."images/user_male.png";
}
$profile->image = $this->generateProfileImage($profile->first_name, $profile->last_name);
}
return $profile;
@@ -172,9 +215,9 @@ class FileService
public function updateProfileImage($profile)
{
$file = new File();
$file->Load('name = ?', array('profile_image_'.$profile->id));
$file->Load('file_group = ? and employee = ?', array('profile_image', $profile->id));
if ($file->name == 'profile_image_'.$profile->id) {
if ($file->employee == $profile->id) {
$uploadFilesToS3 = SettingsManager::getInstance()->getSetting("Files: Upload Files to S3");
if ($uploadFilesToS3 == "1") {
$uploadFilesToS3Key = SettingsManager::getInstance()->getSetting(
@@ -200,11 +243,7 @@ class FileService
$profile->image = CLIENT_BASE_URL.'data/'.$file->filename;
}
} else {
if ($profile->gender == 'Female') {
$profile->image = BASE_URL."images/user_female.png";
} else {
$profile->image = BASE_URL."images/user_male.png";
}
$profile->image = $this->generateProfileImage($profile->first_name, $profile->last_name);
}
return $profile;
@@ -249,26 +288,29 @@ class FileService
public function deleteProfileImage($profileId)
{
$file = new File();
$file->Load('name = ?', array('profile_image_'.$profileId));
if ($file->name == 'profile_image_'.$profileId) {
$ok = $file->Delete();
if ($ok) {
LogManager::getInstance()->info("Delete File:".CLIENT_BASE_PATH.$file->filename);
unlink(CLIENT_BASE_PATH.'data/'.$file->filename);
} else {
return false;
$profilesImages = $file->Find('file_group = ? and employee = ?', array('profile_image', $profileId));
foreach ($profilesImages as $file) {
if ($file->employee == $profileId) {
$ok = $file->Delete();
if ($ok) {
LogManager::getInstance()->info("Delete File:".CLIENT_BASE_PATH.$file->filename);
unlink(CLIENT_BASE_PATH.'data/'.$file->filename);
} else {
return false;
}
}
}
$file = new File();
$file->Load('name = ?', array('profile_image_'.$profileId."_small"));
if ($file->name == 'profile_image_'.$profileId."_small") {
$ok = $file->Delete();
if ($ok) {
LogManager::getInstance()->info("Delete File:".CLIENT_BASE_PATH.$file->filename);
unlink(CLIENT_BASE_PATH.'data/'.$file->filename);
} else {
return false;
$profilesImages = $file->Find('file_group = ? and employee = ?', array('profile_image_small', $profileId));
foreach ($profilesImages as $file) {
if ($file->employee == $profileId) {
$ok = $file->Delete();
if ($ok) {
LogManager::getInstance()->info("Delete File:".CLIENT_BASE_PATH.$file->filename);
unlink(CLIENT_BASE_PATH.'data/'.$file->filename);
} else {
return false;
}
}
}
@@ -330,4 +372,20 @@ class FileService
return round(pow(1024, $base - floor($base)), $precision) .' '. $suffixes[floor($base)];
}
public function generateProfileImage($first, $last)
{
$seed = substr($first, 0, 1);
if (empty($last)) {
$seed .= substr($first, -1);
} else {
$seed .= substr($last, 0, 1);
}
md5($seed . $last);
return sprintf(
'https://avatars.dicebear.com/api/initials/:%s.svg',
$seed . substr(md5($first . $last), -5)
);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Classes;
use Firebase\JWT\JWT;
class JwtTokenService
{
public function create($expire = 3600)
{
$secret = APP_SEC.APP_PASSWORD;
$resp = BaseService::getInstance()->getAccessToken();
$payload = array(
"token" => $resp->getData(),
"expire" => time() + $expire,
);
return JWT::encode($payload, $secret);
}
public function getBaseToken($jwtToken)
{
$secret = APP_SEC.APP_PASSWORD;
$jwt = JWT::decode($jwtToken, $secret, array('HS256'));
if (time() > intval($jwt->expire)) {
return null;
}
return $jwt->token;
}
}

View File

@@ -65,6 +65,7 @@ class ModuleAccessService
return count(array_intersect($userRoles, $moduleUserRoles)) > 0;
}
return in_array($user->user_level, $moduleUserLevels);
return in_array($user->user_level, $moduleUserLevels)
|| count(array_intersect($userRoles, $moduleUserRoles)) > 0;
}
}

View File

@@ -16,7 +16,14 @@ class PermissionManager
{
const RESTRICTED_USER_LEVELS = ['Restricted Admin', 'Restricted Manager', 'Restricted Employee'];
public function isRestrictedUserLevel($userLevel)
const ACCESS_LIST_DESCRIPTION = [
'get' => 'List',
'element' => 'View Details',
'save' => 'Add/Edit',
'delete' => 'Delete',
];
public static function isRestrictedUserLevel($userLevel)
{
return in_array($userLevel, self::RESTRICTED_USER_LEVELS);
}
@@ -50,4 +57,10 @@ class PermissionManager
return $subIds;
}
public static function checkGeneralAccess($object)
{
$currentUser = BaseService::getInstance()->getCurrentUser();
return $object->getRoleBasedAccess($currentUser->user_level, $currentUser->user_roles);
}
}

View File

@@ -178,23 +178,34 @@ class RestEndPoint
protected function getCombinedValue($nameField, $targetObject)
{
$values = explode("+", $nameField);
if (count($values) == 1) {
return $targetObject->{$nameField};
}
$objVal = '';
foreach ($values as $value) {
if ($objVal != "") {
$objVal .= " ";
if (is_string($nameField)) {
$values = explode("+", $nameField);
if (count($values) == 1) {
return $targetObject->{$nameField};
}
if (substr($value, 0, 1) !== ':') {
$objVal .= $targetObject->{$value};
} else {
$objVal .= substr($value, 1);
$objVal = '';
foreach ($values as $value) {
if ($objVal != "") {
$objVal .= " ";
}
if (substr($value, 0, 1) !== ':') {
$objVal .= $targetObject->{$value};
} else {
$objVal .= substr($value, 1);
}
}
return $objVal;
} elseif (is_array($nameField)) {
$objVal = [];
foreach ($nameField as $value) {
$objVal[$value] = $targetObject->{$value};
}
return $objVal;
}
return $objVal;
return null;
}
protected function cleanObject($obj)
@@ -221,7 +232,7 @@ class RestEndPoint
return $obj;
}
public function listAll(User $user)
public function listAll(User $user, $parameter = null)
{
return new IceResponse(IceResponse::ERROR, "Method not Implemented", 404);
}
@@ -403,13 +414,20 @@ class RestEndPoint
private function getBearerToken()
{
$headers = $this->getAuthorizationHeader();
$token = '';
// HEADER: Get the access token from the header
if (!empty($headers)) {
if (preg_match('/Bearer\s(\S+)/', $headers, $matches)) {
return $matches[1];
$token = $matches[1];
}
}
return null;
if (strlen($token) > 32) {
$tokenService = new JwtTokenService();
$token = $tokenService->getBaseToken($token);
}
return $token;
}
protected function getRequestBody()

View File

@@ -0,0 +1,107 @@
<?php
namespace Classes\SystemTasks\DTO;
class Task implements \JsonSerializable
{
const PRIORITY_ERROR = 100;
const PRIORITY_WARNING = 50;
const PRIORITY_INFO = 20;
const PRIORITY_OK = 10;
protected $priority;
protected $text;
protected $link;
protected $action;
protected $details;
/**
* Task constructor.
* @param $priority
* @param $text
*/
public function __construct($priority, $text)
{
$this->priority = $priority;
$this->text = $text;
}
/**
* @return mixed
*/
public function getPriority()
{
return $this->priority;
}
/**
* @param mixed $priority
*/
public function setPriority($priority): Task
{
$this->priority = $priority;
return $this;
}
/**
* @return mixed
*/
public function getText()
{
return $this->text;
}
/**
* @param mixed $text
*/
public function setText($text): Task
{
$this->text = $text;
return $this;
}
/**
* @return mixed
*/
public function getLink()
{
return $this->link;
}
/**
* @param mixed $link
* @param string $action
* @return Task
*/
public function setLink($link, $action = 'Fix'): Task
{
$this->link = $link;
$this->action = $action;
return $this;
}
/**
* @param mixed $details
*/
public function setDetails($details): Task
{
$this->details = $details;
return $this;
}
public function jsonSerialize()
{
return [
'priority' => $this->priority,
'text' => $this->text,
'link' => $this->link,
'action' => $this->action,
'details' => $this->details,
];
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Classes\SystemTasks;
use Classes\SystemTasks\DTO\Task;
class SystemTasksService
{
protected $taskCreators = [];
private static $me = null;
private function __construct()
{
}
public static function getInstance()
{
if (empty(self::$me)) {
self::$me = new SystemTasksService();
}
return self::$me;
}
public function registerTaskCreator(TaskCreator $taskCreator)
{
$this->taskCreators[] = $taskCreator;
}
protected function prepareTaskCreatorCallbacks()
{
$taskGenerators = [];
foreach ($this->taskCreators as $taskCreator) {
$taskList = $taskCreator->getTasksCreators();
foreach ($taskList as $order => $callback) {
$nextOrder = $order * 1000;
while (isset($taskGenerators[$nextOrder])) {
$nextOrder = 1 + $nextOrder;
}
$taskGenerators[$nextOrder] = $callback;
}
}
return $taskGenerators;
}
public function getAdminTasks()
{
$tasks = [];
$taskGenerators = $this->prepareTaskCreatorCallbacks();
ksort($taskGenerators);
foreach ($taskGenerators as $key => $callback) {
$task = $callback();
if (!empty($task)) {
$tasks[] = $task;
}
}
return $tasks;
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Classes\SystemTasks;
interface TaskCreator
{
public function getTasksCreators();
}

View File

@@ -268,7 +268,7 @@ class UIManager
{
$currentCountryCode = $currentLanguage;
if ($currentLanguage === 'en') {
$currentCountryCode = 'gb';
$currentCountryCode = 'un';
} elseif ($currentLanguage === 'zh') {
$currentCountryCode = 'cn';
} elseif ($currentLanguage === 'ja') {