diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 262b69c..1a27960 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -30,7 +30,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. diff --git a/.github/workflows/ossar-analysis.yml b/.github/workflows/ossar-analysis.yml index 8bb6220..c47834d 100644 --- a/.github/workflows/ossar-analysis.yml +++ b/.github/workflows/ossar-analysis.yml @@ -17,7 +17,7 @@ jobs: steps: # Checkout your code repository to scan - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 13947a3..dc01426 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -21,7 +21,7 @@ jobs: name: Testing ${{ matrix.php-versions }} steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Dump GitHub context env: @@ -37,7 +37,7 @@ jobs: coverage: xdebug, pcov - name: Setup NodeJS - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: '14' @@ -60,7 +60,7 @@ jobs: name: Deploy to staging steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -71,7 +71,7 @@ jobs: coverage: xdebug, pcov - name: Setup NodeJS - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: '14' @@ -86,13 +86,13 @@ jobs: php -r 'require("deployment_remotes.php");' cat deployment.log | grep "After-jobs:" ; exit $? - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 if: ${{ always() }} with: name: deploy_php_log_${{ github.run_id }} path: /tmp/php.log - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 if: ${{ always() }} with: name: deploy_log_${{ github.run_id }} @@ -106,7 +106,7 @@ jobs: name: Deploy to production steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -117,7 +117,7 @@ jobs: coverage: xdebug, pcov - name: Setup NodeJS - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: '14' @@ -132,13 +132,13 @@ jobs: php -r 'require("deployment_remotes.php");' cat deployment.log | grep "After-jobs:" ; exit $? - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 if: ${{ always() }} with: name: deploy_php_log_${{ github.run_id }} path: /tmp/php.log - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 if: ${{ always() }} with: name: deploy_log_${{ github.run_id }} diff --git a/backend/alerts.php b/backend/alerts.php new file mode 100644 index 0000000..4c9bb7d --- /dev/null +++ b/backend/alerts.php @@ -0,0 +1,415 @@ +selectValue("SELECT COUNT(id) FROM `".DB_PREFIX."_profiles` WHERE `available` = 1") < 2) { + throw new NotEnoughAvailableUsersException(); + return; + } + + $chief_result = $db->selectRow("SELECT * FROM `".DB_PREFIX."_profiles` WHERE `hidden` = 0 AND `available` = 1 AND `chief` = 1 ORDER BY services ASC, trainings DESC, availability_minutes ASC, name ASC LIMIT 1"); + if(is_null($chief_result)) { + throw new NoChiefAvailableException(); + return; + } + $crew[] = $chief_result; + if($chief_result["driver"]) { + $result = $db->select("SELECT * FROM `".DB_PREFIX."_profiles` WHERE `hidden` = 0 AND `available` = 1 ORDER BY chief ASC, services ASC, trainings DESC, availability_minutes ASC, name ASC"); + foreach ($result as $row) { + if(!in_array($row["id"], array_column($crew, 'id'))) { + $crew[] = $row; + } + } + } else { + $driver_result = $db->selectRow("SELECT * FROM `".DB_PREFIX."_profiles` WHERE `hidden` = 0 AND `available` = 1 AND `driver` = 1 ORDER BY chief ASC, services ASC, trainings DESC, availability_minutes ASC, name ASC"); + if(is_null($driver_result)) { + throw new NoDriverAvailableException(); + return; + } + foreach ($driver_result as $row) { + if(!in_array($row["id"], array_column($crew, 'id'))) { + $crew[] = $row; + } + } + } + + if ($type == 'full') { + $result = $db->select("SELECT * FROM `".DB_PREFIX."_profiles` WHERE `hidden` = 0 AND `available` = 1 ORDER BY chief ASC, services ASC, trainings DESC, availability_minutes ASC, name ASC"); + foreach ($result as $row) { + if(!in_array($row["id"], array_column($crew, 'id'))) { + $crew[] = $row; + } + } + } + + return $crew; +} + +function loadCrewMemberData($input) { + global $db; + $result = $db->selectRow("SELECT * FROM `".DB_PREFIX."_profiles` WHERE `id` = ?", [$input["id"]]); + if(is_null($result)) { + throw new Exception("Crew member not found"); + return; + } + return array_merge($input, $result); +} + +function updateAlertMessages($alert, $crew=null, $alertDeleted = false) { + global $Bot, $users, $db; + + if(is_null($Bot)) initializeBot(NONE); + + if(is_null($crew)) { + $crew = json_decode($alert["crew"], true); + } + + $notification_messages = json_decode($alert["notification_messages"], true); + $notification_text = generateAlertReportMessage($alert["type"], $crew, $alert["enabled"], $alert["notes"], $alert["created_by"], $alertDeleted); + foreach($notification_messages as $chat_id => $message_id) { + try { + $Bot->editMessageText([ + "chat_id" => $chat_id, + "message_id" => $message_id, + "text" => $notification_text + ]); + } catch(skrtdev\Telegram\BadRequestException) { + // + } + } + + if($alertDeleted) { + foreach($crew as &$member) { + $message_id = $member["telegram_message_id"]; + $chat_id = $member["telegram_chat_id"]; + + if(!is_null($message_id) && !is_null($chat_id)) { + $Bot->sendMessage([ + "chat_id" => $chat_id, + "text" => "Allerta rimossa.\nPartecipazione non più richiesta.", + "reply_to_message_id" => $message_id + ]); + try { + $Bot->editMessageReplyMarkup([ + "chat_id" => $chat_id, + "message_id" => $message_id, + "reply_markup" => [ + 'inline_keyboard' => [ + ] + ] + ]); + } catch(skrtdev\Telegram\BadRequestException) { + // + } + } + } + + return; + } + + $available_users_count = 0; + $drivers_count = 0; + $chiefs_count = 0; + foreach($crew as &$member) { + if($member["response"] === true) { + $user = $users->getUserById($member["id"]); + $available_users_count++; + if($user["driver"]) $drivers_count++; + if($user["chief"]) $chiefs_count++; + } + } + + if( + ($alert["type"] === "support" && $available_users_count >= 2 && $chiefs_count >= 1 && $drivers_count >= 1) || + ($alert["type"] === "full" && $available_users_count >= 5 && $chiefs_count >= 1 && $drivers_count >= 1) + ) { + $db->update( + DB_PREFIX."_alerts", + [ + "enabled" => 0 + ], + [ + "id" => $alert["id"] + ] + ); + + $notification_text = generateAlertReportMessage($alert["type"], $crew, false, $alert["notes"], $alert["created_by"], $alertDeleted); + foreach($notification_messages as $chat_id => $message_id) { + try { + $Bot->editMessageText([ + "chat_id" => $chat_id, + "message_id" => $message_id, + "text" => $notification_text + ]); + } catch(skrtdev\Telegram\BadRequestException) { + // + } + } + + foreach($crew as &$member) { + $message_id = $member["telegram_message_id"]; + $chat_id = $member["telegram_chat_id"]; + + if((!is_null($message_id) || !is_null($chat_id)) && $member["response"] === "waiting") { + $Bot->sendMessage([ + "chat_id" => $chat_id, + "text" => "Numero minimo vigili richiesti raggiunto.\nPartecipazione non più richiesta.", + "reply_to_message_id" => $message_id + ]); + try { + $Bot->editMessageReplyMarkup([ + "chat_id" => $chat_id, + "message_id" => $message_id, + "reply_markup" => [ + 'inline_keyboard' => [ + ] + ] + ]); + } catch(skrtdev\Telegram\BadRequestException) { + // + } + } + } + } +} + +function setAlertResponse($response, $userId, $alertId) { + global $db, $users, $Bot; + + if(is_null($Bot)) initializeBot(NONE); + + $alert = $db->selectRow( + "SELECT * FROM `".DB_PREFIX."_alerts` WHERE `id` = ?", [$alertId] + ); + + if(!$alert["enabled"]) return; + + $crew = json_decode($alert["crew"], true); + $messageText = $response ? "🟢 Partecipazione accettata." : "🔴 Partecipazione rifiutata."; + + foreach($crew as &$member) { + if($member["id"] == $userId) { + if($member["response"] === $response) return; + + $message_id = $member["telegram_message_id"]; + $chat_id = $member["telegram_chat_id"]; + + if(!is_null($message_id) || !is_null($chat_id)) { + $Bot->sendMessage([ + "chat_id" => $chat_id, + "text" => $messageText, + "reply_to_message_id" => $message_id + ]); + try { + $Bot->editMessageReplyMarkup([ + "chat_id" => $chat_id, + "message_id" => $message_id, + "reply_markup" => [ + 'inline_keyboard' => [ + ] + ] + ]); + } catch(skrtdev\Telegram\BadRequestException) { + // + } + } + + $member["response"] = $response; + $member["response_time"] = get_timestamp(); + } + } + $db->update( + DB_PREFIX."_alerts", + [ + "crew" => json_encode($crew) + ], + [ + "id" => $alertId + ] + ); + + updateAlertMessages($alert, $crew); +} + +function alertsRouter (FastRoute\RouteCollector $r) { + $r->addRoute( + 'GET', + '', + function ($vars) { + global $db, $users; + requireLogin(); + $alerts = $db->select("SELECT * FROM `".DB_PREFIX."_alerts` WHERE `enabled` = 1"); + if(is_null($alerts)) $alerts = []; + foreach($alerts as &$alert) { + if(isset($_GET["load_less"])) { + $alert = [ + "id" => $alert["id"], + "created_at" => $alert["created_at"] + ]; + } else { + $alert["crew"] = json_decode($alert["crew"], true); + $alert["crew"] = array_map(function($crew_member) { + return loadCrewMemberData($crew_member); + }, $alert["crew"]); + } + } + apiResponse($alerts); + } + ); + + $r->addRoute( + 'POST', + '', + function ($vars) { + global $db, $users; + requireLogin(); + $users->online_time_update(); + if(!$users->hasRole(Role::SUPER_EDITOR)) { + apiResponse(["status" => "error", "message" => "Access denied"]); + return; + } + + try { + $crew_members = callsList($_POST["type"]); + } catch (NoChiefAvailableException) { + apiResponse(["status" => "error", "message" => "Nessun caposquadra disponibile. Contattare i vigili manualmente."]); + return; + } catch (NoDriverAvailableException) { + apiResponse(["status" => "error", "message" => "Nessun autista disponibile. Contattare i vigili manualmente."]); + return; + } catch (NotEnoughAvailableUsersException) { + apiResponse(["status" => "error", "message" => "Nessun utente disponibile. Distaccamento non operativo."]); + return; + } + + $crew = []; + foreach($crew_members as $member) { + $crew[] = [ + "id" => $member["id"], + "response" => "waiting" + ]; + } + + $notifications = sendAlertReportMessage($_POST["type"], $crew, true, "", $users->auth->getUserId()); + + $db->insert( + DB_PREFIX."_alerts", + [ + "crew" => json_encode($crew), + "type" => $_POST["type"], + "created_at" => get_timestamp(), + "created_by" => $users->auth->getUserId(), + "notification_messages" => json_encode($notifications) + ] + ); + $alertId = $db->getLastInsertId(); + + foreach($crew as &$member) { + [$member["telegram_message_id"], $member["telegram_chat_id"]] = sendAlertRequestMessage($_POST["type"], $member["id"], $alertId, "", $users->auth->getUserId()); + } + + $db->update( + DB_PREFIX."_alerts", + [ + "crew" => json_encode($crew) + ], + [ + "id" => $alertId + ] + ); + + apiResponse([ + "crew" => $crew, + "id" => $alertId + ]); + } + ); + + $r->addRoute( + 'GET', + '/{id:\d+}', + function ($vars) { + global $db; + requireLogin(); + $alert = $db->selectRow("SELECT * FROM `".DB_PREFIX."_alerts` WHERE `id` = :id", [":id" => $vars["id"]]); + if(is_null($alert)) { + apiResponse(["error" => "alert not found"]); + return; + } + $alert["crew"] = json_decode($alert["crew"], true); + $alert["crew"] = array_map(function($crew_member) { + return loadCrewMemberData($crew_member); + }, $alert["crew"]); + apiResponse($alert); + } + ); + + $r->addRoute( + 'POST', + '/{id:\d+}/settings', + function ($vars) { + global $db, $users; + requireLogin(); + $users->online_time_update(); + if(!$users->hasRole(Role::SUPER_EDITOR)) { + apiResponse(["status" => "error", "message" => "Access denied"]); + return; + } + $db->update( + DB_PREFIX."_alerts", + [ + "notes" => $_POST["notes"] + ], + [ + "id" => $vars["id"] + ] + ); + + $alert = $db->selectRow( + "SELECT * FROM `".DB_PREFIX."_alerts` WHERE `id` = :id", + [ + ":id" => $vars["id"] + ] + ); + updateAlertMessages($alert); + } + ); + + $r->addRoute( + 'DELETE', + '/{id:\d+}', + function ($vars) { + global $db, $users; + requireLogin(); + $users->online_time_update(); + if(!$users->hasRole(Role::SUPER_EDITOR)) { + apiResponse(["status" => "error", "message" => "Access denied"]); + return; + } + $db->update( + DB_PREFIX."_alerts", + [ + "enabled" => 0 + ], + [ + "id" => $vars["id"] + ] + ); + + $alert = $db->selectRow( + "SELECT * FROM `".DB_PREFIX."_alerts` WHERE `id` = :id", + [ + ":id" => $vars["id"] + ] + ); + updateAlertMessages($alert, null, true); + } + ); +} \ No newline at end of file diff --git a/backend/apiRouter.php b/backend/apiRouter.php index af4e489..276edcf 100644 --- a/backend/apiRouter.php +++ b/backend/apiRouter.php @@ -2,12 +2,17 @@ require_once 'utils.php'; require_once 'telegramBotRouter.php'; require_once 'cronRouter.php'; +require_once 'alerts.php'; function apiRouter (FastRoute\RouteCollector $r) { $r->addGroup('/cron', function (FastRoute\RouteCollector $r) { cronRouter($r); }); + $r->addGroup('/alerts', function (FastRoute\RouteCollector $r) { + alertsRouter($r); + }); + $r->addRoute( ['GET', 'POST'], '/bot/telegram', @@ -94,9 +99,9 @@ function apiRouter (FastRoute\RouteCollector $r) { '/list', function ($vars) { global $db, $users; - requireLogin() || accessDenied(); + requireLogin(); $users->online_time_update(); - if($users->hasRole(Role::FULL_VIEWER)) { + if($users->hasRole(Role::SUPER_EDITOR)) { $response = $db->select("SELECT * FROM `".DB_PREFIX."_profiles` WHERE `hidden` = 0 ORDER BY available DESC, chief DESC, services ASC, trainings DESC, availability_minutes ASC, name ASC"); } else { $response = $db->select("SELECT `id`, `chief`, `online_time`, `available`, `availability_minutes`, `name`, `driver`, `services` FROM `".DB_PREFIX."_profiles` WHERE `hidden` = 0 ORDER BY available DESC, chief DESC, services ASC, trainings DESC, availability_minutes ASC, name ASC"); @@ -112,7 +117,7 @@ function apiRouter (FastRoute\RouteCollector $r) { '/logs', function ($vars) { global $db, $users; - requireLogin() || accessDenied(); + requireLogin(); $users->online_time_update(); $response = $db->select("SELECT * FROM `".DB_PREFIX."_log` ORDER BY `timestamp` DESC"); if(!is_null($response)) { @@ -132,7 +137,7 @@ function apiRouter (FastRoute\RouteCollector $r) { '/services', function ($vars) { global $services, $users; - requireLogin() || accessDenied(); + requireLogin(); $users->online_time_update(); apiResponse($services->list()); } @@ -142,7 +147,7 @@ function apiRouter (FastRoute\RouteCollector $r) { '/services', function ($vars) { global $services, $users; - requireLogin() || accessDenied(); + requireLogin(); $users->online_time_update(); apiResponse(["response" => $services->add($_POST["start"], $_POST["end"], $_POST["code"], $_POST["chief"], $_POST["drivers"], $_POST["crew"], $_POST["place"], $_POST["notes"], $_POST["type"], $users->auth->getUserId())]); } @@ -153,7 +158,7 @@ function apiRouter (FastRoute\RouteCollector $r) { '/services/{id}', function ($vars) { global $services, $users; - requireLogin() || accessDenied(); + requireLogin(); $users->online_time_update(); apiResponse($services->get($vars['id'])); } @@ -163,7 +168,7 @@ function apiRouter (FastRoute\RouteCollector $r) { '/services/{id}', function ($vars) { global $services, $users; - requireLogin() || accessDenied(); + requireLogin(); $users->online_time_update(); apiResponse(["response" => $services->delete($vars["id"])]); } @@ -174,7 +179,7 @@ function apiRouter (FastRoute\RouteCollector $r) { '/place_details', function ($vars) { global $db, $users; - requireLogin() || accessDenied(); + requireLogin(); $users->online_time_update(); $response = $db->selectRow("SELECT * FROM `".DB_PREFIX."_places_info` WHERE `lat` = ? and `lng` = ? LIMIT 0,1;", [$_GET["lat"], $_GET["lng"]]); apiResponse(!is_null($response) ? $response : []); @@ -186,7 +191,7 @@ function apiRouter (FastRoute\RouteCollector $r) { '/trainings', function ($vars) { global $db, $users; - requireLogin() || accessDenied(); + requireLogin(); $users->online_time_update(); $response = $db->select("SELECT * FROM `".DB_PREFIX."_trainings` ORDER BY date DESC, beginning desc"); apiResponse( @@ -200,7 +205,7 @@ function apiRouter (FastRoute\RouteCollector $r) { '/users', function ($vars) { global $users, $users; - requireLogin() || accessDenied(); + requireLogin(); $users->online_time_update(); apiResponse($users->get_users()); } @@ -210,8 +215,8 @@ function apiRouter (FastRoute\RouteCollector $r) { '/users', function ($vars) { global $users; - requireLogin() || accessDenied(); - if(!$users->hasRole(Role::FULL_VIEWER) && $_POST["id"] !== $users->auth->getUserId()){ + requireLogin(); + if(!$users->hasRole(Role::SUPER_EDITOR) && $_POST["id"] !== $users->auth->getUserId()){ exit; } apiResponse(["userId" => $users->add_user($_POST["email"], $_POST["name"], $_POST["username"], $_POST["password"], $_POST["phone_number"], $_POST["birthday"], $_POST["chief"], $_POST["driver"], $_POST["hidden"], $_POST["disabled"], "unknown")]); @@ -222,11 +227,11 @@ function apiRouter (FastRoute\RouteCollector $r) { '/users/{userId}', function ($vars) { global $users; - requireLogin() || accessDenied(); - if(!$users->hasRole(Role::FULL_VIEWER) && $_POST["id"] !== $users->auth->getUserId()){ + requireLogin(); + if(!$users->hasRole(Role::SUPER_EDITOR) && $_POST["id"] !== $users->auth->getUserId()){ exit; } - apiResponse($users->get_user($vars["userId"])); + apiResponse($users->getUserById($vars["userId"])); } ); $r->addRoute( @@ -234,8 +239,8 @@ function apiRouter (FastRoute\RouteCollector $r) { '/users/{userId}', function ($vars) { global $users; - requireLogin() || accessDenied(); - if(!$users->hasRole(Role::FULL_VIEWER) && $_POST["id"] !== $users->auth->getUserId()){ + requireLogin(); + if(!$users->hasRole(Role::SUPER_EDITOR) && $_POST["id"] !== $users->auth->getUserId()){ exit; } $users->remove_user($vars["userId"], "unknown"); @@ -248,7 +253,7 @@ function apiRouter (FastRoute\RouteCollector $r) { '/availability', function ($vars) { global $users, $db; - requireLogin() || accessDenied(); + requireLogin(); $users->online_time_update(); $row = $db->selectRow( "SELECT `available`, `manual_mode` FROM `".DB_PREFIX."_profiles` WHERE `id` = ?", @@ -265,10 +270,12 @@ function apiRouter (FastRoute\RouteCollector $r) { '/availability', function ($vars) { global $users, $availability; - requireLogin() || accessDenied(); + requireLogin(); $users->online_time_update(); - if(!$users->hasRole(Role::FULL_VIEWER) && $_POST["id"] !== $users->auth->getUserId()){ - exit; + if(!$users->hasRole(Role::SUPER_EDITOR) && (int) $_POST["id"] !== $users->auth->getUserId()){ + statusCode(401); + apiResponse(["status" => "error", "message" => "You don't have permission to change other users availability", "t" => $users->auth->getUserId()]); + return; } $user_id = is_numeric($_POST["id"]) ? $_POST["id"] : $users->auth->getUserId(); apiResponse([ @@ -283,7 +290,7 @@ function apiRouter (FastRoute\RouteCollector $r) { "/manual_mode", function ($vars) { global $users, $availability; - requireLogin() || accessDenied(); + requireLogin(); $users->online_time_update(); $availability->change_manual_mode($_POST["manual_mode"]); apiResponse(["status" => "success"]); @@ -295,7 +302,7 @@ function apiRouter (FastRoute\RouteCollector $r) { '/schedules', function ($vars) { global $users, $schedules; - requireLogin() || accessDenied(); + requireLogin(); $users->online_time_update(); apiResponse($schedules->get()); } @@ -305,7 +312,7 @@ function apiRouter (FastRoute\RouteCollector $r) { '/schedules', function ($vars) { global $users, $schedules; - requireLogin() || accessDenied(); + requireLogin(); $users->online_time_update(); $new_schedules = !is_string($_POST["schedules"]) ? json_encode($_POST["schedules"]) : $_POST["schedules"]; apiResponse([ @@ -319,7 +326,7 @@ function apiRouter (FastRoute\RouteCollector $r) { '/service_types', function ($vars) { global $users, $db; - requireLogin() || accessDenied(); + requireLogin(); $users->online_time_update(); $response = $db->select("SELECT * FROM `".DB_PREFIX."_type`"); apiResponse(is_null($response) ? [] : $response); @@ -330,7 +337,7 @@ function apiRouter (FastRoute\RouteCollector $r) { '/service_types', function ($vars) { global $users, $db; - requireLogin() || accessDenied(); + requireLogin(); $users->online_time_update(); $response = $db->insert(DB_PREFIX."_type", ["name" => $_POST["name"]]); apiResponse($response); @@ -342,7 +349,7 @@ function apiRouter (FastRoute\RouteCollector $r) { '/places/search', function ($vars) { global $places; - requireLogin() || accessDenied(); + requireLogin(); apiResponse($places->search($_GET["q"])); } ); @@ -352,7 +359,7 @@ function apiRouter (FastRoute\RouteCollector $r) { '/telegram_login_token', function ($vars) { global $users, $db; - requireLogin() || accessDenied(); + requireLogin(); $users->online_time_update(); $token = bin2hex(random_bytes(16)); apiResponse([ @@ -405,6 +412,64 @@ function apiRouter (FastRoute\RouteCollector $r) { } } ); + $r->addRoute( + ['POST'], + '/impersonate', + function ($vars) { + global $users; + requireLogin(); + + if(!$users->hasRole(Role::SUPER_ADMIN)) { + statusCode(401); + apiResponse(["status" => "error", "message" => "You don't have permission to impersonate"]); + return; + } + + try { + $token = $users->loginAsUserIdAndReturnToken($_POST["user_id"]); + apiResponse(["status" => "success", "access_token" => $token]); + } + catch (\Delight\Auth\UnknownIdException $e) { + statusCode(400); + apiResponse(["status" => "error", "message" => "Wrong user ID"]); + } + catch (\Delight\Auth\EmailNotVerifiedException $e) { + statusCode(400); + apiResponse(["status" => "error", "message" => "Email not verified"]); + } + catch (Exception $e) { + statusCode(400); + apiResponse(["status" => "error", "message" => "Unknown error", "error" => $e]); + } + } + ); + $r->addRoute( + ['POST'], + '/stop_impersonating', + function ($vars) { + global $users; + requireLogin(); + + if(array_key_exists("impersonating_user", $users->auth->user_info) && array_key_exists("precedent_user_id", $users->auth->user_info)) { + $precedent_user_id = $users->auth->user_info["precedent_user_id"]; + $users->auth->logOut(); + $token = $users->loginAsUserIdAndReturnToken($precedent_user_id); + apiResponse(["status" => "success", "access_token" => $token, "user_id" => $users->auth->getUserId()]); + } + } + ); + $r->addRoute( + ['GET', 'POST'], + '/refreshToken', + function ($vars) { + global $users; + requireLogin(false); + + apiResponse([ + "token" => $users->generateToken() + ]); + } + ); $r->addRoute( ['GET', 'POST'], '/validateToken', diff --git a/backend/composer.json b/backend/composer.json index 9060018..035544a 100644 --- a/backend/composer.json +++ b/backend/composer.json @@ -13,11 +13,11 @@ "delight-im/auth": "dev-master", "ulrichsg/getopt-php": "4.0.1", "nikic/fast-route": "^2.0@dev", - "spatie/array-to-xml": "3.1.0", + "spatie/array-to-xml": "3.1.1", "ezyang/htmlpurifier": "4.14.0", "brick/phonenumber": "0.4.0", "sentry/sdk": "3.1.1", - "azuyalabs/yasumi": "2.4.0", + "azuyalabs/yasumi": "2.5.0", "ministryofweb/php-osm-tiles": "2.0.0", "delight-im/db": "1.3.1", "phpfastcache/phpfastcache": "9.0.1", diff --git a/backend/composer.lock b/backend/composer.lock index 2fbe9e3..059c369 100644 --- a/backend/composer.lock +++ b/backend/composer.lock @@ -4,34 +4,34 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9193956804bd765f7fe3f29d0c61472e", + "content-hash": "e757f460b505a5b23599ce4866a7a957", "packages": [ { "name": "azuyalabs/yasumi", - "version": "2.4.0", + "version": "2.5.0", "source": { "type": "git", "url": "https://github.com/azuyalabs/yasumi.git", - "reference": "083a0d0579fee17e68d688d463bc01098ac2691f" + "reference": "5fd99815e8bf480fd0e6b76527d5413767e98930" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/azuyalabs/yasumi/zipball/083a0d0579fee17e68d688d463bc01098ac2691f", - "reference": "083a0d0579fee17e68d688d463bc01098ac2691f", + "url": "https://api.github.com/repos/azuyalabs/yasumi/zipball/5fd99815e8bf480fd0e6b76527d5413767e98930", + "reference": "5fd99815e8bf480fd0e6b76527d5413767e98930", "shasum": "" }, "require": { "ext-json": "*", - "php": ">=7.3" + "php": ">=7.4" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.16", - "infection/infection": "^0.17 | ^0.22", + "friendsofphp/php-cs-fixer": "v2.19 | v3.5", + "infection/infection": "^0.17 | ^0.26", "mikey179/vfsstream": "^1.6", - "phan/phan": "^4.0", - "phpstan/phpstan": "^0.12.66", - "phpunit/phpunit": "^8.5 | ^9.4", - "vimeo/psalm": "^4" + "phan/phan": "^5.2", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^8.5 | ^9.5", + "vimeo/psalm": "^4.9" }, "suggest": { "ext-calendar": "For calculating the date of Easter" @@ -77,7 +77,7 @@ "type": "other" } ], - "time": "2021-05-09T09:03:34+00:00" + "time": "2022-01-30T07:43:17+00:00" }, { "name": "brick/phonenumber", @@ -157,12 +157,12 @@ }, "type": "library", "autoload": { - "psr-4": { - "Clue\\StreamFilter\\": "src/" - }, "files": [ "src/functions_include.php" - ] + ], + "psr-4": { + "Clue\\StreamFilter\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -207,12 +207,12 @@ "source": { "type": "git", "url": "https://github.com/allerta-vvf/PHP-Auth-JWT", - "reference": "a39b9e746d056145c31bb9c72f613c751d85e105" + "reference": "f5a99a4502ed05a4707de610114fd2426b2629ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/allerta-vvf/PHP-Auth-JWT/zipball/a39b9e746d056145c31bb9c72f613c751d85e105", - "reference": "a39b9e746d056145c31bb9c72f613c751d85e105", + "url": "https://api.github.com/repos/allerta-vvf/PHP-Auth-JWT/zipball/f5a99a4502ed05a4707de610114fd2426b2629ef", + "reference": "f5a99a4502ed05a4707de610114fd2426b2629ef", "shasum": "" }, "require": { @@ -240,7 +240,7 @@ "login", "security" ], - "time": "2022-01-09T13:37:14+00:00" + "time": "2022-02-14T10:21:41+00:00" }, { "name": "delight-im/base64", @@ -385,16 +385,16 @@ }, { "name": "giggsey/libphonenumber-for-php", - "version": "8.12.41", + "version": "8.12.43", "source": { "type": "git", "url": "https://github.com/giggsey/libphonenumber-for-php.git", - "reference": "c7b9f89a25e37e8bb650a378c3eabcbdafedeafd" + "reference": "27bc97a4941f42d320fb6da3de0dcaaf7db69f5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/giggsey/libphonenumber-for-php/zipball/c7b9f89a25e37e8bb650a378c3eabcbdafedeafd", - "reference": "c7b9f89a25e37e8bb650a378c3eabcbdafedeafd", + "url": "https://api.github.com/repos/giggsey/libphonenumber-for-php/zipball/27bc97a4941f42d320fb6da3de0dcaaf7db69f5d", + "reference": "27bc97a4941f42d320fb6da3de0dcaaf7db69f5d", "shasum": "" }, "require": { @@ -454,7 +454,7 @@ "issues": "https://github.com/giggsey/libphonenumber-for-php/issues", "source": "https://github.com/giggsey/libphonenumber-for-php" }, - "time": "2022-01-11T10:10:37+00:00" + "time": "2022-02-09T07:46:55+00:00" }, { "name": "giggsey/locale", @@ -1208,12 +1208,12 @@ } }, "autoload": { - "psr-4": { - "FastRoute\\": "src/" - }, "files": [ "src/functions.php" - ] + ], + "psr-4": { + "FastRoute\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1443,16 +1443,16 @@ }, { "name": "php-http/message", - "version": "1.12.0", + "version": "1.13.0", "source": { "type": "git", "url": "https://github.com/php-http/message.git", - "reference": "39eb7548be982a81085fe5a6e2a44268cd586291" + "reference": "7886e647a30a966a1a8d1dad1845b71ca8678361" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/message/zipball/39eb7548be982a81085fe5a6e2a44268cd586291", - "reference": "39eb7548be982a81085fe5a6e2a44268cd586291", + "url": "https://api.github.com/repos/php-http/message/zipball/7886e647a30a966a1a8d1dad1845b71ca8678361", + "reference": "7886e647a30a966a1a8d1dad1845b71ca8678361", "shasum": "" }, "require": { @@ -1469,7 +1469,7 @@ "ext-zlib": "*", "guzzlehttp/psr7": "^1.0", "laminas/laminas-diactoros": "^2.0", - "phpspec/phpspec": "^5.1 || ^6.3", + "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", "slim/slim": "^3.0" }, "suggest": { @@ -1485,12 +1485,12 @@ } }, "autoload": { - "psr-4": { - "Http\\Message\\": "src/" - }, "files": [ "src/filters.php" - ] + ], + "psr-4": { + "Http\\Message\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1511,9 +1511,9 @@ ], "support": { "issues": "https://github.com/php-http/message/issues", - "source": "https://github.com/php-http/message/tree/1.12.0" + "source": "https://github.com/php-http/message/tree/1.13.0" }, - "time": "2021-08-29T09:13:12+00:00" + "time": "2022-02-11T13:41:14+00:00" }, { "name": "php-http/message-factory", @@ -2336,13 +2336,13 @@ }, "type": "library", "autoload": { - "psr-4": { - "skrtdev\\async\\": "src/" - }, "files": [ "src/range.php", "src/helpers.php" - ] + ], + "psr-4": { + "skrtdev\\async\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2424,16 +2424,16 @@ }, { "name": "spatie/array-to-xml", - "version": "3.1.0", + "version": "3.1.1", "source": { "type": "git", "url": "https://github.com/spatie/array-to-xml.git", - "reference": "3090918cb441ad707660dd8bccc6dc46beb34380" + "reference": "18d474f5d53d3ff8d98e7ca00781d84c9c98d286" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/3090918cb441ad707660dd8bccc6dc46beb34380", - "reference": "3090918cb441ad707660dd8bccc6dc46beb34380", + "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/18d474f5d53d3ff8d98e7ca00781d84c9c98d286", + "reference": "18d474f5d53d3ff8d98e7ca00781d84c9c98d286", "shasum": "" }, "require": { @@ -2472,7 +2472,7 @@ ], "support": { "issues": "https://github.com/spatie/array-to-xml/issues", - "source": "https://github.com/spatie/array-to-xml/tree/3.1.0" + "source": "https://github.com/spatie/array-to-xml/tree/3.1.1" }, "funding": [ { @@ -2484,7 +2484,7 @@ "type": "github" } ], - "time": "2021-09-12T17:08:24+00:00" + "time": "2021-11-22T19:44:12+00:00" }, { "name": "symfony/deprecation-contracts", @@ -2555,16 +2555,16 @@ }, { "name": "symfony/http-client", - "version": "v6.0.2", + "version": "v6.0.3", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "7f1cbd44590cb0acc6208c1711a52733e9a91663" + "reference": "45b95017f6a20d564584bdee6a376c9a79caa316" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/7f1cbd44590cb0acc6208c1711a52733e9a91663", - "reference": "7f1cbd44590cb0acc6208c1711a52733e9a91663", + "url": "https://api.github.com/repos/symfony/http-client/zipball/45b95017f6a20d564584bdee6a376c9a79caa316", + "reference": "45b95017f6a20d564584bdee6a376c9a79caa316", "shasum": "" }, "require": { @@ -2619,7 +2619,7 @@ "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-client/tree/v6.0.2" + "source": "https://github.com/symfony/http-client/tree/v6.0.3" }, "funding": [ { @@ -2635,7 +2635,7 @@ "type": "tidelift" } ], - "time": "2021-12-29T10:14:09+00:00" + "time": "2022-01-22T06:58:00+00:00" }, { "name": "symfony/http-client-contracts", @@ -2717,16 +2717,16 @@ }, { "name": "symfony/options-resolver", - "version": "v6.0.0", + "version": "v6.0.3", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "be0facf48a42a232d6c0daadd76e4eb5657a4798" + "reference": "51f7006670febe4cbcbae177cbffe93ff833250d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/be0facf48a42a232d6c0daadd76e4eb5657a4798", - "reference": "be0facf48a42a232d6c0daadd76e4eb5657a4798", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/51f7006670febe4cbcbae177cbffe93ff833250d", + "reference": "51f7006670febe4cbcbae177cbffe93ff833250d", "shasum": "" }, "require": { @@ -2764,7 +2764,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v6.0.0" + "source": "https://github.com/symfony/options-resolver/tree/v6.0.3" }, "funding": [ { @@ -2780,7 +2780,7 @@ "type": "tidelift" } ], - "time": "2021-11-23T19:05:29+00:00" + "time": "2022-01-02T09:55:41+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -2816,12 +2816,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2893,12 +2893,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -2982,12 +2982,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Uuid\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ diff --git a/backend/cronRouter.php b/backend/cronRouter.php index 832fb17..e5a0ec6 100644 --- a/backend/cronRouter.php +++ b/backend/cronRouter.php @@ -173,7 +173,7 @@ function job_send_notification_if_manual_mode() { $profiles = $db->select("SELECT * FROM `".DB_PREFIX."_profiles` WHERE `manual_mode` = 1"); $notified_users = []; foreach ($profiles as $profile) { - $notified_users[] = $profiles["id"]; + $notified_users[] = $profile["id"]; $stato = $profile["available"] ? "disponibile" : "non disponibile"; sendTelegramNotificationToUser("⚠️ Attenzione! La tua disponibilità non segue la programmazione oraria.\nAttualmente sei {$stato}.\nScrivi \"/programma\" se vuoi ripristinare la programmazione.", $profile["id"]); } diff --git a/backend/router.php b/backend/router.php index cb60be4..814707b 100644 --- a/backend/router.php +++ b/backend/router.php @@ -130,12 +130,33 @@ function getBearerToken() { return null; } -function requireLogin() +function requireLogin($validate_token_version=true) { global $users; $token = getBearerToken(); if($users->auth->isTokenValid($token)) { $users->auth->authenticateWithToken($token); + if($users->auth->hasRole(\Delight\Auth\Role::CONSULTANT)) { + //Migrate to new user roles + $users->auth->admin()->removeRoleForUserById($users->auth->getUserId(), \Delight\Auth\Role::CONSULTANT); + $users->auth->admin()->addRoleForUserById($users->auth->getUserId(), Role::SUPER_EDITOR); + + $users->auth->authenticateWithToken($token); + } + + if($validate_token_version) { + if(!array_key_exists("v", $users->auth->user_info)) { + statusCode(400); + apiResponse(["status" => "error", "message" => "JWT client version is not supported", "type" => "jwt_update_required"]); + exit(); + } + if((int) $users->auth->user_info["v"] !== 2) { + statusCode(400); + apiResponse(["status" => "error", "message" => "JWT client version ".$users->auth->user_info["v"]." is not supported", "type" => "jwt_update_required"]); + exit(); + } + } + if(defined('SENTRY_LOADED')) { \Sentry\configureScope(function (\Sentry\State\Scope $scope) use ($users): void { $scope->setUser([ @@ -147,15 +168,11 @@ function requireLogin() ]); }); } - return true; - } - return false; -} -function accessDenied() -{ + return; + } statusCode(401); - apiResponse(["error" => "Access denied"]); + apiResponse(["status" => "error", "message" => "Access denied"]); exit(); } @@ -195,7 +212,7 @@ try { break; case FastRoute\Dispatcher::METHOD_NOT_ALLOWED: $allowedMethods = $routeInfo[1]; - http_response_code(405); + statusCode(405); apiResponse(["status" => "error", "message" => "Method not allowed", "usedMethod" => $_SERVER['REQUEST_METHOD']]); break; case FastRoute\Dispatcher::FOUND: diff --git a/backend/telegramBotRouter.php b/backend/telegramBotRouter.php index fe7bd85..37131e6 100644 --- a/backend/telegramBotRouter.php +++ b/backend/telegramBotRouter.php @@ -1,6 +1,7 @@ selectValue("SELECT user FROM `".DB_PREFIX."_bot_telegram` WHERE `chat_id` = ?", [$message->from->id]); + return $db->selectValue("SELECT user FROM `".DB_PREFIX."_bot_telegram` WHERE `chat_id` = ?", [$from_id]); +} + +function getUserIdByMessage(Message $message) +{ + return getUserIdByFrom($message->from->id); } function requireBotLogin(Message $message) { + global $users; + $userId = getUserIdByMessage($message); if ($userId === null) { $message->reply( @@ -47,23 +55,31 @@ function requireBotLogin(Message $message) "\nPer farlo, premere su \"Collega l'account al bot Telegram\"." ); exit(); + } else { + if($users->auth->hasRole(\Delight\Auth\Role::CONSULTANT)) { + //Migrate to new user roles + $users->auth->admin()->removeRoleForUserById($users->auth->getUserId(), \Delight\Auth\Role::CONSULTANT); + $users->auth->admin()->addRoleForUserById($users->auth->getUserId(), Role::SUPER_EDITOR); + } } } -function sendTelegramNotification($message) +function sendTelegramNotification($message, $do_not_send_if_same=true) { global $Bot, $db; if(is_null($Bot)) initializeBot(NONE); + $sentMessages = []; + //TODO: implement different types of notifications //TODO: add command for subscribing to notifications $chats = $db->select("SELECT * FROM `".DB_PREFIX."_bot_telegram_notifications`"); if(!is_null($chats)) { foreach ($chats as $chat) { - if(urldecode($chat['last_notification']) === $message) continue; + if($do_not_send_if_same && urldecode($chat['last_notification']) === $message) continue; $chat = $chat['chat_id']; - $Bot->sendMessage([ + $sendMessage = $Bot->sendMessage([ "chat_id" => $chat, "text" => $message ]); @@ -72,11 +88,13 @@ function sendTelegramNotification($message) ["last_notification" => urlencode($message)], ["chat_id" => $chat] ); + $sentMessages[$chat] = $sendMessage->message_id; } } + return $sentMessages; } -function sendTelegramNotificationToUser($message, $userId) +function sendTelegramNotificationToUser($message, $userId, $options = []) { global $Bot, $db; @@ -84,18 +102,100 @@ function sendTelegramNotificationToUser($message, $userId) $chat = $db->selectValue("SELECT `chat_id` FROM `".DB_PREFIX."_bot_telegram` WHERE `user` = ?", [$userId]); if(!is_null($chat)) { - $Bot->sendMessage([ + $message_response = $Bot->sendMessage(array_merge([ "chat_id" => $chat, "text" => $message - ]); + ], $options)); + return [$message_response->message_id, $chat]; } } +function generateAlertMessage($alertType, $alertEnabled, $alertNotes, $alertCreatedBy, $alertDeleted=false) { + global $users; + + $message = + "".($alertEnabled ? "Allertamento in corso" : ($alertDeleted ? "Allertamento completato" : "Allerta rimossa")).": ". + ($alertType === "full" ? "Richiesta squadra completa 🚒" : "Supporto 🧯\n"); + + if(!is_null($alertNotes) && $alertNotes !== "") { + $message .= "Note:\n".$alertNotes."\n"; + } + if(!is_null($alertCreatedBy)) { + $message .= "Lanciata da: ".$users->getName($alertCreatedBy)."\n"; + } + + return $message; +} + +function generateAlertReportMessage($alertType, $crew, $alertEnabled, $alertNotes, $alertCreatedBy, $alertDeleted=false) { + global $users; + + $message = generateAlertMessage($alertType, $alertEnabled, $alertNotes, $alertCreatedBy); + $message .= "\nSquadra:\n"; + + foreach($crew as $member) { + if((!$alertEnabled || $alertDeleted) && $member["response"] === "waiting") continue; + $user = $users->getUserById($member['id']); + $message .= "".$user["name"]." "; + if($user["chief"]) $message .= "CS"; + if($user["driver"]) $message .= "🚒"; + $message .= "- "; + if($member["response"] === "waiting") { + $message .= "In attesa 🟡"; + } else if($member["response"] === true) { + $message .= "Presente 🟢"; + } else if($member["response"] === false) { + $message .= "Assente 🔴"; + } + $message .= "\n"; + } + + return $message; +} + +function sendAlertReportMessage($alertType, $crew, $alertEnabled, $alertNotes, $alertCreatedBy, $alertDeleted = false) { + $message = generateAlertReportMessage($alertType, $crew, $alertEnabled, $alertNotes, $alertCreatedBy, $alertDeleted); + + return sendTelegramNotification($message, false); +} + +function sendAlertRequestMessage($alertType, $userId, $alertId, $alertNotes, $alertCreatedBy, $alertDeleted = false) { + return sendTelegramNotificationToUser(generateAlertMessage($alertType, true, $alertNotes, $alertCreatedBy, $alertDeleted), $userId, [ + 'reply_markup' => [ + 'inline_keyboard' => [ + [ + [ + 'text' => '✅ Partecipo', + 'callback_data' => "alert_yes_".$alertId + ], + [ + 'text' => 'Non partecipo ❌', + 'callback_data' => "alert_no_".$alertId + ] + ] + ] + ] + ]); +} + function yesOrNo($value) { return ($value === 1 || $value) ? 'SI' : 'NO'; } +function sendLongMessage($text, $userId) { + global $Bot; + if(strlen($text) > 4096) { + $message_json = wordwrap($text, 4096, "<@MESSAGE_SEPARATOR@>", true); + $message_json = explode("<@MESSAGE_SEPARATOR@>", $message_json); + foreach($message_json as $segment) { + sendLongMessage($segment, $userId); + } + } else { + $Bot->sendMessage($userId, $text); + } +} + function telegramBotRouter() { global $Bot; @@ -158,13 +258,42 @@ function telegramBotRouter() { ); }); + $Bot->onCommand('debug_userid', function (Message $message) { + global $Bot; + + $messageText = "🔎 ID utente Telegram: ".$message->from->id.""; + if(isset($message->from->username)) { + $messageText .= "\n💬 Username: ".$message->from->username.""; + } + if(isset($message->from->first_name)) { + $messageText .= "\n🔎 Nome: ".$message->from->first_name.""; + } + if(isset($message->from->last_name)) { + $messageText .= "\n🔎 Cognome: ".$message->from->last_name.""; + } + if(isset($message->from->language_code)) { + $messageText .= "\n🌐 Lingua: ".$message->from->language_code.""; + } + if(isset($message->from->is_bot)) { + $messageText .= "\n🤖 Bot: ".yesOrNo($message->from->is_bot).""; + } + $message->reply($messageText); + + if(defined("BOT_TELEGRAM_DEBUG_USER") && BOT_TELEGRAM_DEBUG_USER !== $message->from->id){ + $messageText .= "\n\n🔎 JSON del messaggio:"; + $Bot->sendMessage(BOT_TELEGRAM_DEBUG_USER, $messageText); + $message_json = json_encode($message, JSON_PRETTY_PRINT); + sendLongMessage($message_json, BOT_TELEGRAM_DEBUG_USER); + } + }); + $Bot->onCommand('info', function (Message $message) { global $users; $user_id = getUserIdByMessage($message); if(is_null($user_id)) { $message->chat->sendMessage('⚠️ Questo account Telegram non è associato a nessun utente di Allerta.'); } else { - $user = $users->get_user($user_id); + $user = $users->getUserById($user_id); $message->chat->sendMessage( "ℹ️ Informazioni sul profilo:". "\nNome: ".$user["name"]."". @@ -240,6 +369,20 @@ function telegramBotRouter() { } $message->reply($msg); }); + + $Bot->onCallbackQuery(function (CallbackQuery $callback_query) use ($Bot) { + $user = $callback_query->from; + $message = $callback_query->message; + $chat = $message->chat; + + if(strpos($callback_query->data, 'alert_') === 0) { + $data = explode("_", str_replace("alert_", "", $callback_query->data)); + $alert_id = $data[1]; + + setAlertResponse($data[0] === "yes", getUserIdByFrom($user->id), $alert_id); + return; + } + }); $Bot->start(); -} \ No newline at end of file +} diff --git a/backend/utils.php b/backend/utils.php index 7dd1661..f81f094 100644 --- a/backend/utils.php +++ b/backend/utils.php @@ -74,15 +74,14 @@ $auth = new \Delight\Auth\Auth($db, $JWTconfig, get_ip(), DB_PREFIX."_"); final class Role { - //https://github.com/delight-im/PHP-Auth/blob/master/src/Role.php - const GUEST = \Delight\Auth\Role::AUTHOR; - const BASIC_VIEWER = \Delight\Auth\Role::COLLABORATOR; - const FULL_VIEWER = \Delight\Auth\Role::CONSULTANT; - const EDITOR = \Delight\Auth\Role::CONSUMER; - const SUPER_EDITOR = \Delight\Auth\Role::CONTRIBUTOR; + const EDITOR = \Delight\Auth\Role::EDITOR; + const SUPER_EDITOR = \Delight\Auth\Role::SUPER_EDITOR; + const DEVELOPER = \Delight\Auth\Role::DEVELOPER; - const TESTER = \Delight\Auth\Role::CREATOR; + + const GUEST = \Delight\Auth\Role::SUBSCRIBER; const EXTERNAL_VIEWER = \Delight\Auth\Role::REVIEWER; + const ADMIN = \Delight\Auth\Role::ADMIN; const SUPER_ADMIN = \Delight\Auth\Role::SUPER_ADMIN; @@ -92,6 +91,10 @@ final class Role } +function get_timestamp() { + return round(microtime(true) * 1000); +} + function logger($action, $changed=null, $editor=null, $timestamp=null, $source_type="api") { global $db, $users; @@ -191,7 +194,7 @@ class Users ["hidden" => $hidden, "disabled" => $disabled, "name" => $name, "phone_number" => $phone_number, "chief" => $chief, "driver" => $driver] ); if($chief == 1) { - $this->auth->admin()->addRoleForUserById($userId, Role::FULL_VIEWER); + $this->auth->admin()->addRoleForUserById($userId, Role::SUPER_EDITOR); } logger("User added", $userId, $inserted_by); return $userId; @@ -205,7 +208,7 @@ class Users return $this->db->select("SELECT * FROM `".DB_PREFIX."_profiles` WHERE `hidden` = 0"); } - public function get_user($id) + public function getUserById($id) { return $this->db->selectRow("SELECT * FROM `".DB_PREFIX."_profiles` WHERE `id` = ?", [$id]); } @@ -233,16 +236,61 @@ class Users ); } + public function generateToken($precedent_user_id = null) + { + $token_params = [ + "roles" => $this->auth->getRoles(), + "name" => $this->getName(), + "v" => 2 + ]; + if(!is_null($precedent_user_id)) { + $token_params["impersonating_user"] = true; + $token_params["precedent_user_id"] = $precedent_user_id; + } + $token = $this->auth->generateJWTtoken($token_params); + return $token; + } + public function loginAndReturnToken($username, $password) { $this->auth->loginWithUsername($username, $password); - $token = $this->auth->generateJWTtoken([ - "full_viewer" => $this->hasRole(Role::FULL_VIEWER), - "name" => $this->getName(), - ]); - return $token; + + if($this->auth->hasRole(\Delight\Auth\Role::CONSULTANT)) { + //Migrate to new user roles + $this->auth->admin()->removeRoleForUserById($this->auth->getUserId(), \Delight\Auth\Role::CONSULTANT); + $this->auth->admin()->addRoleForUserById($this->auth->getUserId(), Role::SUPER_EDITOR); + + $this->auth->loginWithUsername($username, $password); + } + + return $this->generateToken(); } + public function loginAsUserIdAndReturnToken($userId) + { + $precedent_user_id = null; + if(!is_null($this->auth->getUserId())) { + if((int) $userId === (int) $this->auth->getUserId()) { + return $this->generateToken(); + } + $precedent_user_id = $this->auth->getUserId(); + $this->auth->logOut(); + } + + $this->auth->admin()->logInAsUserById($userId); + + if($this->auth->hasRole(\Delight\Auth\Role::CONSULTANT)) { + //Migrate to new user roles + $this->auth->admin()->removeRoleForUserById($this->auth->getUserId(), \Delight\Auth\Role::CONSULTANT); + $this->auth->admin()->addRoleForUserById($this->auth->getUserId(), Role::SUPER_EDITOR); + + $this->auth->admin()->logInAsUserById($userId); + } + + return $this->generateToken($precedent_user_id); + } + + public function isHidden($id=null) { if(is_null($id)) $id = $this->auth->getUserId(); @@ -303,10 +351,10 @@ class Availability { $available_users_count = $this->db->selectValue("SELECT COUNT(id) FROM `".DB_PREFIX."_profiles` WHERE `available` = 1 AND `hidden` = 0"); if($available_users_count === 5) { sendTelegramNotification("🚒 Distaccamento operativo con squadra completa"); - } else if($available_users_count === 2) { - sendTelegramNotification("🧯 Distaccamento operativo per supporto"); - } else if($available_users_count === 1 && !$availability) { + } else if($available_users_count < 2) { sendTelegramNotification("⚠️ Distaccamento non operativo"); + } else if($available_users_count < 5) { + sendTelegramNotification("🧯 Distaccamento operativo per supporto"); } } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index cad9bc0..bf64e8e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -20,6 +20,8 @@ "@asymmetrik/ngx-leaflet": "^8.1.0", "@fortawesome/fontawesome-free": "^5.15.4", "@ng-bootstrap/ng-bootstrap": "11.0.0", + "@ngx-translate/core": "^14.0.0", + "@ngx-translate/http-loader": "^7.0.0", "bootstrap": "^5.1.3", "jwt-decode": "^3.1.2", "leaflet": "^1.7.1", @@ -2467,6 +2469,31 @@ "webpack": "^5.30.0" } }, + "node_modules/@ngx-translate/core": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-14.0.0.tgz", + "integrity": "sha512-UevdwNCXMRCdJv//0kC8h2eSfmi02r29xeE8E9gJ1Al4D4jEJ7eiLPdjslTMc21oJNGguqqWeEVjf64SFtvw2w==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": ">=13.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@ngx-translate/http-loader": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-7.0.0.tgz", + "integrity": "sha512-j+NpXXlcGVdyUNyY/qsJrqqeAdJdizCd+GKh3usXExSqy1aE9866jlAIL+xrfDU4w+LiMoma5pgE4emvFebZmA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=13.0.0", + "@ngx-translate/core": ">=14.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -14331,6 +14358,22 @@ "dev": true, "requires": {} }, + "@ngx-translate/core": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-14.0.0.tgz", + "integrity": "sha512-UevdwNCXMRCdJv//0kC8h2eSfmi02r29xeE8E9gJ1Al4D4jEJ7eiLPdjslTMc21oJNGguqqWeEVjf64SFtvw2w==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@ngx-translate/http-loader": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-7.0.0.tgz", + "integrity": "sha512-j+NpXXlcGVdyUNyY/qsJrqqeAdJdizCd+GKh3usXExSqy1aE9866jlAIL+xrfDU4w+LiMoma5pgE4emvFebZmA==", + "requires": { + "tslib": "^2.3.0" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index b14c751..d2c582c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,6 +24,8 @@ "@asymmetrik/ngx-leaflet": "^8.1.0", "@fortawesome/fontawesome-free": "^5.15.4", "@ng-bootstrap/ng-bootstrap": "11.0.0", + "@ngx-translate/core": "^14.0.0", + "@ngx-translate/http-loader": "^7.0.0", "bootstrap": "^5.1.3", "jwt-decode": "^3.1.2", "leaflet": "^1.7.1", diff --git a/frontend/src/app/_components/back-btn/back-btn.component.html b/frontend/src/app/_components/back-btn/back-btn.component.html index 40d2ac3..ca585f2 100644 --- a/frontend/src/app/_components/back-btn/back-btn.component.html +++ b/frontend/src/app/_components/back-btn/back-btn.component.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/src/app/_components/back-btn/back-btn.module.ts b/frontend/src/app/_components/back-btn/back-btn.module.ts index 637cd1b..d0111c7 100644 --- a/frontend/src/app/_components/back-btn/back-btn.module.ts +++ b/frontend/src/app/_components/back-btn/back-btn.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { TranslationModule } from '../../translation.module'; import { BackBtnComponent } from './back-btn.component'; @@ -8,7 +9,8 @@ import { BackBtnComponent } from './back-btn.component'; BackBtnComponent ], imports: [ - CommonModule + CommonModule, + TranslationModule ], exports: [ BackBtnComponent diff --git a/frontend/src/app/_components/datetime-picker/datetime-picker.component.html b/frontend/src/app/_components/datetime-picker/datetime-picker.component.html index 03c5f40..d84b3aa 100644 --- a/frontend/src/app/_components/datetime-picker/datetime-picker.component.html +++ b/frontend/src/app/_components/datetime-picker/datetime-picker.component.html @@ -1,5 +1,5 @@
-
\ No newline at end of file diff --git a/frontend/src/app/_components/datetime-picker/datetime-picker.component.ts b/frontend/src/app/_components/datetime-picker/datetime-picker.component.ts index 66b1948..98cf370 100644 --- a/frontend/src/app/_components/datetime-picker/datetime-picker.component.ts +++ b/frontend/src/app/_components/datetime-picker/datetime-picker.component.ts @@ -27,7 +27,7 @@ export class DatetimePickerComponent implements OnInit, ControlValueAccessor { } ngOnInit(): void { - this.localeService.use('it'); + this.localeService.use(window.navigator.language.split("-")[0]); } get value(): Date { diff --git a/frontend/src/app/_components/datetime-picker/datetime-picker.module.ts b/frontend/src/app/_components/datetime-picker/datetime-picker.module.ts index 82b6f43..511fa0d 100644 --- a/frontend/src/app/_components/datetime-picker/datetime-picker.module.ts +++ b/frontend/src/app/_components/datetime-picker/datetime-picker.module.ts @@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { BsDatepickerModule } from 'ngx-bootstrap/datepicker'; +import { TranslationModule } from '../../translation.module'; import { DatetimePickerComponent } from './datetime-picker.component'; @@ -13,7 +14,8 @@ import { DatetimePickerComponent } from './datetime-picker.component'; imports: [ CommonModule, FormsModule, - BsDatepickerModule.forRoot() + BsDatepickerModule.forRoot(), + TranslationModule ], exports: [ DatetimePickerComponent diff --git a/frontend/src/app/_components/list/list.component.html b/frontend/src/app/_components/list/list.component.html deleted file mode 100644 index 01feac2..0000000 --- a/frontend/src/app/_components/list/list.component.html +++ /dev/null @@ -1,24 +0,0 @@ -
-

Attualmente sei: {{ available ? "Disponibile" : "Non disponibile" }}{{ manual_mode ? "" : " (programmato)" }}

-
- - -
- - - -
-
- -
- - -
- -
\ No newline at end of file diff --git a/frontend/src/app/_components/map-picker/map-picker.component.html b/frontend/src/app/_components/map-picker/map-picker.component.html index 20cc58c..5d39b13 100644 --- a/frontend/src/app/_components/map-picker/map-picker.component.html +++ b/frontend/src/app/_components/map-picker/map-picker.component.html @@ -3,8 +3,8 @@