diff --git a/WEB-INF/resources/en.lang.php b/WEB-INF/resources/en.lang.php index 5b433b0a..9bb42523 100644 --- a/WEB-INF/resources/en.lang.php +++ b/WEB-INF/resources/en.lang.php @@ -83,6 +83,9 @@ 'error.format' => 'Invalid file format.', 'error.user_count' => 'Limit on user count.', 'error.expired' => 'Expiration date reached.', +'error.jwt_token_not_found' => 'JWT not found in request.', +'error.jwt_invalid_token' => 'JWT is not valid.', +'error.api_no_handler' => 'Invalid request, endpoint %s is not valid', // Meaning of error.file_storage: an (unspecified) error occurred when trying to communicate with remote // file storage server (the one that handles attachments). It is a generic message telling us that // "something went wrong" when trying to do some operation with attachments. diff --git a/api.php b/api.php new file mode 100644 index 00000000..54d2c0f1 --- /dev/null +++ b/api.php @@ -0,0 +1,105 @@ +doLogin($body['username'], $body['password']); + if ($loginSucceeded) { + $user = new ttUser(null, $auth->getUserId()); + echo json_encode(array('token'=>generate_decoded_token($user))); + } else { + send_error('error.auth'); + } + exit; +}else { + $decodedTokenExistsInHeader = preg_match('/Bearer\s(\S+)/', $_SERVER['HTTP_AUTHORIZATION'], $matcheddecodedTokenArr); + if (! $decodedTokenExistsInHeader) { + send_error('error.jwt_token_not_found'); + exit; + }else { + $decodedToken = decode_token($matcheddecodedTokenArr[1]); + if(! validate_decodedToken($decodedToken)){ + exit; + } + $user = new ttUser(null, $decodedToken->sub->id); + route_request($domain,$params,$body,$user); + } +} + +function generate_decoded_token($user) { + $current_ts = (new DateTimeImmutable())->getTimestamp(); + $payload = [ + 'iss' => $_SERVER['SERVER_NAME'], + 'aud' => $_SERVER['SERVER_NAME'], + 'iat' => $current_ts, + 'nbf' => $current_ts, + 'sub' => get_subject($user), + 'exp' => $current_ts+(3600*24) + ]; + return JWT::encode($payload, JWT_KEY, JWT_ALGO); +} + +function get_subject($user) { + return array( + 'login' => $user->login, + 'name' => $user->name, + 'id' => $user->id, + 'org_id' => $user->org_id, + 'group_id' => $user->group_id, + 'group_name' => $user->group_name, + 'role_id' => $user->role_id, + 'role_name' => $user->role_name, + 'rank' => $user->rank, + 'email' => $user->email, + ); +} + +function decode_token($jwt) { + return JWT::decode($jwt, new key (JWT_KEY, JWT_ALGO)); +} + +function validate_decodedToken($decodedToken) { + $now = new DateTimeImmutable(); + $serverName = $_SERVER['SERVER_NAME']; + if ($decodedToken->iss !== $serverName || + $decodedToken->nbf > $now->getTimestamp() || + $decodedToken->exp < $now->getTimestamp()) { + send_error('error.jwt_invalid_token'); + return false; + } + + return true; +} + +function route_request($domain, $params, $body, $user) { + $routeHandler = "handle_req_".$domain; + if(function_exists($routeHandler)) { + $routeHandler($params,$body,$user); + }else{ + send_error('error.api_no_handler', $domain); + } + +} \ No newline at end of file diff --git a/api_installation.txt b/api_installation.txt new file mode 100644 index 00000000..7401154c --- /dev/null +++ b/api_installation.txt @@ -0,0 +1,10 @@ +Copy all files with api_*.php +Add config to WEBINF/config.php +Copy WEBINF/resource/en.lang.php + +// API Module +define('JWT_KEY', 'LjtkLTGGSUudOxI03tcoZdf3yy8vgdMS7PlZaXDB'); +define('JWT_ALGO', 'HS256'); + + +Copy composer-lock and composer.json files \ No newline at end of file diff --git a/api_lib.php b/api_lib.php new file mode 100644 index 00000000..70e14e6d --- /dev/null +++ b/api_lib.php @@ -0,0 +1,7 @@ +$i18n->get($error, $args))); +} \ No newline at end of file diff --git a/api_route_clients.php b/api_route_clients.php new file mode 100644 index 00000000..c575aade --- /dev/null +++ b/api_route_clients.php @@ -0,0 +1,14 @@ +0) { + $action = $params[0]; + } + switch($action) { + case 'list': + echo json_encode(ttGroupHelper::getActiveClients(true)); + break; + } +} diff --git a/api_route_projects.php b/api_route_projects.php new file mode 100644 index 00000000..0a4ac24b --- /dev/null +++ b/api_route_projects.php @@ -0,0 +1,31 @@ +0) { + $action = $params[0]; + } + + switch($action) { + case 'list': + echo json_encode(list_projects($user)); + break; + } +} + +function list_projects($user) { + $config = new ttConfigHelper($user->getConfig()); + + if ($user->can('track_time')) { + $rank = $user->getMaxRankForGroup($group_id); + if ($user->can('track_own_time')) + $options = array('status'=>ACTIVE,'max_rank'=>$rank,'include_self'=>true,'self_first'=>true); + else + $options = array('status'=>ACTIVE,'max_rank'=>$rank); + $user_list = $user->getUsers($options); + } + $options['include_templates'] = $user->isPluginEnabled('tp') && $config->getDefinedValue('bind_templates_with_projects'); + return $user->getAssignedProjects($options); +} \ No newline at end of file diff --git a/api_route_time.php b/api_route_time.php new file mode 100644 index 00000000..b5af0f8e --- /dev/null +++ b/api_route_time.php @@ -0,0 +1,135 @@ +0) { + $action = $params[0]; + } + + switch($action) { + case 'add': + echo json_encode(add_time_entry($body)); + break; + } +} + +function add_time_entry($time_entry) { + global $user, $err, $i18n; + $cl_client = null; $cl_task = null; $cl_project = null; $cl_start = null; $cl_finish = null; $cl_duration = null; $cl_billable=1; + + $config = new ttConfigHelper($user->getConfig()); + $showClient = $user->isPluginEnabled('cl'); + $trackingMode = $user->getTrackingMode(); + $showProject = MODE_PROJECTS == $trackingMode || MODE_PROJECTS_AND_TASKS == $trackingMode; + $showTask = MODE_PROJECTS_AND_TASKS == $trackingMode; + $taskRequired = false; + $recordType = $user->getRecordType(); + $showStart = TYPE_START_FINISH == $recordType || TYPE_ALL == $recordType; + $showDuration = TYPE_DURATION == $recordType || TYPE_ALL == $recordType; + $oneUncompleted = $config->getDefinedValue('one_uncompleted'); + + extract($time_entry, EXTR_OVERWRITE); + $selected_date = new ttDate($cl_date); + + if ($showTask) $taskRequired = $config->getDefinedValue('task_required'); + + // Validate user input. + if ($showClient && $user->isOptionEnabled('client_required') && !$cl_client){ + $i18n->add('error.client'); + return; + } + // Validate input in time custom fields. + if (isset($custom_fields) && $custom_fields->timeFields) { + foreach ($timeCustomFields as $timeField) { + // Validation is the same for text and dropdown fields. + if (!ttValidString($timeField['value'], !$timeField['required'])) $err->add($i18n->get('error.field'), htmlspecialchars($timeField['label'])); + } + } + if ($showProject) { + if (!$cl_project) $err->add($i18n->get('error.project')); + } + if ($showTask && $taskRequired) { + if (!$cl_task) $err->add($i18n->get('error.task')); + } + if (strlen($cl_duration) == 0) { + if ($cl_start || $cl_finish) { + if (!ttTimeHelper::isValidTime($cl_start)) + $err->add($i18n->get('error.field'), $i18n->get('label.start')); + if ($cl_finish) { + if (!ttTimeHelper::isValidTime($cl_finish)) + $err->add($i18n->get('error.field'), $i18n->get('label.finish')); + if (!ttTimeHelper::isValidInterval($cl_start, $cl_finish)) + $err->add($i18n->get('error.interval'), $i18n->get('label.finish'), $i18n->get('label.start')); + } + } else { + if ($showStart) { + $err->add($i18n->get('error.empty'), $i18n->get('label.start')); + $err->add($i18n->get('error.empty'), $i18n->get('label.finish')); + } + if ($showDuration) + $err->add($i18n->get('error.empty'), $i18n->get('label.duration')); + } + } else { + if (false === ttTimeHelper::postedDurationToMinutes($cl_duration)) + $err->add($i18n->get('error.field'), $i18n->get('label.duration')); + } + if (!ttValidString($cl_note, true)) $err->add($i18n->get('error.field'), $i18n->get('label.note')); + if ($user->isPluginEnabled('tp') && !ttValidTemplateText($cl_note)) { + $err->add($i18n->get('error.field'), $i18n->get('label.note')); + } + // Finished validating user input. + + // Prohibit creating entries in future. + if (!$user->isOptionEnabled('future_entries')) { + $server_tomorrow = new ttDate(); + $server_tomorrow->incrementDay(); + if ($selected_date->after($server_tomorrow)) + $err->add($i18n->get('error.future_date')); + } + + // Prohibit creating entries in locked range. + if ($user->isDateLocked($selected_date)) + $err->add($i18n->get('error.range_locked')); + + // Prohibit creating another uncompleted record. + if ($err->no() && $oneUncompleted) { + if (($not_completed_rec = ttTimeHelper::getUncompleted($user_id)) && (($cl_finish == '') && ($cl_duration == ''))) + $err->add($i18n->get('error.uncompleted_exists')." ".$i18n->get('error.goto_uncompleted').""); + } + + // Prohibit creating an overlapping record. + if ($err->no()) { + if (ttTimeHelper::overlaps($user_id, $cl_date, $cl_start, $cl_finish)) + $err->add($i18n->get('error.overlap')); + } + + // Insert record. + if ($err->no()) { + $id = ttTimeHelper::insert(array( + 'date' => $cl_date, + 'client' => $cl_client, + 'project' => $cl_project, + 'task' => $cl_task, + 'start' => $cl_start, + 'finish' => $cl_finish, + 'duration' => $cl_duration, + 'note' => $cl_note, + 'billable' => $cl_billable)); + + // Insert time custom fields if we have them. + $result = true; + if ($id && isset($custom_fields) && $custom_fields->timeFields) { + $result = $custom_fields->insertTimeFields($id, $timeCustomFields); + } + + if ($id && $result && $err->no()) { + return array('id'=> $id,'result' => $result); + } + $err->add($i18n->get('error.db')); + + } + + return array('error'=>$err->getErrors()); +} \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..36557161 --- /dev/null +++ b/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "firebase/php-jwt": "^6.0" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 00000000..e0aeaf37 --- /dev/null +++ b/composer.lock @@ -0,0 +1,76 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "0f2c3a727a6da4acd150a914dd810575", + "packages": [ + { + "name": "firebase/php-jwt", + "version": "v6.0.0", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "0541cba75ab108ef901985e68055a92646c73534" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/0541cba75ab108ef901985e68055a92646c73534", + "reference": "0541cba75ab108ef901985e68055a92646c73534", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": ">=4.8 <=9" + }, + "suggest": { + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v6.0.0" + }, + "time": "2022-01-24T15:18:34+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.2.0" +}