Merge pull request #466 from allerta-vvf/master

Pushing to prod without testing
This commit is contained in:
Matteo Gheza 2022-03-15 15:05:31 +01:00 committed by GitHub
commit 0d0949acf2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
185 changed files with 1889 additions and 9329 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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 }}

415
backend/alerts.php Normal file
View File

@ -0,0 +1,415 @@
<?php
require_once 'utils.php';
final class NoChiefAvailableException extends Exception {}
final class NoDriverAvailableException extends Exception {}
final class NotEnoughAvailableUsersException extends Exception {}
function callsList($type) {
global $db;
$crew = [];
if($db->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);
}
);
}

View File

@ -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',

View File

@ -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",

148
backend/composer.lock generated
View File

@ -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": [

View File

@ -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à <b>non segue la programmazione oraria</b>.\nAttualmente sei <b>{$stato}</b>.\nScrivi \"/programma\" se vuoi ripristinare la programmazione.", $profile["id"]);
}

View File

@ -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:

View File

@ -1,6 +1,7 @@
<?php
use skrtdev\NovaGram\Bot;
use skrtdev\Telegram\Message;
use skrtdev\Telegram\CallbackQuery;
require_once 'utils.php';
@ -32,14 +33,21 @@ function initializeBot($mode = WEBHOOK) {
}
}
function getUserIdByMessage(Message $message)
function getUserIdByFrom($from_id)
{
global $db;
return $db->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 <strong>\"Collega l'account al bot Telegram\"</strong>."
);
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 =
"<b><i><u>".($alertEnabled ? "Allertamento in corso" : ($alertDeleted ? "Allertamento completato" : "Allerta rimossa")).":</u></i></b> ".
($alertType === "full" ? "Richiesta <b>squadra completa 🚒</b>" : "<b>Supporto 🧯</b>\n");
if(!is_null($alertNotes) && $alertNotes !== "") {
$message .= "Note:\n<b>".$alertNotes."</b>\n";
}
if(!is_null($alertCreatedBy)) {
$message .= "Lanciata da: <b>".$users->getName($alertCreatedBy)."</b>\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 .= "<i>".$user["name"]."</i> ";
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) ? '<b>SI</b>' : '<b>NO</b>';
}
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: <b>".$message->from->id."</b>";
if(isset($message->from->username)) {
$messageText .= "\n💬 Username: <b>".$message->from->username."</b>";
}
if(isset($message->from->first_name)) {
$messageText .= "\n🔎 Nome: <b>".$message->from->first_name."</b>";
}
if(isset($message->from->last_name)) {
$messageText .= "\n🔎 Cognome: <b>".$message->from->last_name."</b>";
}
if(isset($message->from->language_code)) {
$messageText .= "\n🌐 Lingua: <b>".$message->from->language_code."</b>";
}
if(isset($message->from->is_bot)) {
$messageText .= "\n🤖 Bot: <b>".yesOrNo($message->from->is_bot)."</b>";
}
$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:".
"\n<i>Nome:</i> <b>".$user["name"]."</b>".
@ -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();
}
}

View File

@ -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");
}
}

View File

@ -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",

View File

@ -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",

View File

@ -1 +1 @@
<button (click)="locationBackService.goBack()" id="backBtn" title="Go back"><i class="fas fa-arrow-left"></i> Torna indietro</button>
<button (click)="locationBackService.goBack()" id="backBtn" [title]="'go_back'|translate|titlecase"><i class="fas fa-arrow-left"></i> {{ 'go_back'|translate|titlecase }}</button>

View File

@ -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

View File

@ -1,5 +1,5 @@
<div class="input-group">
<input type="text" [disabled]="disabled" placeholder="Premi per selezionare data" class="form-control date-picker"
<input type="text" [disabled]="disabled" [placeholder]="'press_to_select_a_date'|translate|titlecase" class="form-control date-picker"
bsDatepicker [bsConfig]="{ adaptivePosition: true, dateInputFormat: 'DD/MM/YYYY' }" [(ngModel)]="date" (ngModelChange)=updateValue()>
<input type="time" [disabled]="disabled" class="form-control" [(ngModel)]="time" (change)=updateValue()>
</div>

View File

@ -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 {

View File

@ -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

View File

@ -1,24 +0,0 @@
<div class="text-center">
<h3 *ngIf="available !== undefined">Attualmente sei: <b>{{ available ? "Disponibile" : "Non disponibile" }}{{ manual_mode ? "" : " (programmato)" }}</b></h3>
<div id="availability-btn-group">
<button (click)="changeAvailibility(1)" type="button" [delay]="1000" tooltip="Cambia la tua disponibilità in 'attivo'" id="activate-btn" class="btn btn-lg btn-success me-1">Attiva</button>
<button (click)="changeAvailibility(0)" type="button" [delay]="1000" tooltip="Cambia la tua disponibilità in 'non attivo'" id="deactivate-btn" class="btn btn-lg btn-danger">Disattiva</button>
</div>
<ng-container *ngIf="manual_mode !== undefined">
<button type="button" class="btn btn-secondary" *ngIf="manual_mode" (click)="updateManualMode(0)">
Attiva programmazione oraria
</button>
<button type="button" class="btn btn-secondary" *ngIf="!manual_mode" (click)="updateManualMode(1)">
Disattiva programmazione oraria
</button>
<br>
</ng-container>
<button type="button" class="btn btn-lg" (click)="openScheduleModal()">
Modifica orari disponibilità
</button>
</div>
<owner-image></owner-image>
<app-table [sourceType]="'list'" (changeAvailability)="changeAvailibility($event.newState, $event.user)" #table></app-table>
<div class="text-center">
<button (click)="requestTelegramToken()" class="btn btn-md btn-success mt-3">Collega l'account al bot Telegram</button>
</div>

View File

@ -3,8 +3,8 @@
</div>
<div id="search" class="mt-2 mb-3">
<div class="input-group mb-3">
<input type="text" class="form-control" placeholder="Luogo" [(ngModel)]="placeName" (keyup.enter)="searchPlace()">
<button class="btn btn-outline-secondary" type="button" (click)="searchPlace()">Cerca</button>
<input type="text" class="form-control" [placeholder]="'place'|translate|titlecase" [(ngModel)]="placeName" (keyup.enter)="searchPlace()">
<button class="btn btn-outline-secondary" type="button" (click)="searchPlace()">{{ 'search'|translate|titlecase }}</button>
</div>
<div id="results" *ngIf="isPlaceSearchResultsOpen">
<li *ngFor="let result of placeSearchResults" (click)="selectPlace(result)">{{ result.display_name }}</li>

View File

@ -1,5 +1,6 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { ToastrService } from 'ngx-toastr';
import { TranslateService } from '@ngx-translate/core';
import { ApiClientService } from 'src/app/_services/api-client.service';
import { LatLng, latLng, tileLayer, Marker, Map } from 'leaflet';
import "leaflet.locatecontrol";
@ -10,8 +11,11 @@ import "leaflet.locatecontrol";
styleUrls: ['./map-picker.component.scss']
})
export class MapPickerComponent implements OnInit {
@Input() lat = 45.88283872530;
@Input() lng = 10.18226623535;
lat = 45.88283872530;
lng = 10.18226623535;
@Input() selectLat = "";
@Input() selectLng = "";
@Output() onMarkerSet = new EventEmitter<any>();
@ -31,12 +35,17 @@ export class MapPickerComponent implements OnInit {
isPlaceSearchResultsOpen = false;
placeSearchResults: any[] = [];
constructor(private toastr: ToastrService, private api: ApiClientService) {
constructor(private toastr: ToastrService, private api: ApiClientService, private translate: TranslateService) {
this.marker = (window as any).L.marker(latLng(0,0));
this.map = undefined as unknown as Map;
}
ngOnInit(): void { }
ngOnInit() {
if(this.selectLat !== "" && this.selectLng !== "") {
console.log(this.selectLat, this.selectLng);
this.setMarker(latLng(parseFloat(this.selectLat), parseFloat(this.selectLng)));
}
}
setMarker(latLng: LatLng) {
this.onMarkerSet.emit({
@ -87,7 +96,9 @@ export class MapPickerComponent implements OnInit {
searchPlace() {
if(this.placeName.length < 3) {
this.toastr.error("Il nome della località deve essere di almeno 3 caratteri");
this.translate.get('map_picker.place_min_length').subscribe((res: string) => {
this.toastr.error(res);
});
return;
}
this.api.get("places/search", {
@ -97,7 +108,9 @@ export class MapPickerComponent implements OnInit {
this.placeSearchResults = places;
}).catch((err) => {
console.error(err);
this.toastr.error("Errore di caricamento dei risultati della ricerca. Riprovare più tardi");
this.translate.get('map_picker.loading_error').subscribe((res: string) => {
this.toastr.error(res);
});
});
}

View File

@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { LeafletModule } from '@asymmetrik/ngx-leaflet';
import { TranslationModule } from '../../translation.module';
import { MapPickerComponent } from './map-picker.component';
@ -13,7 +14,8 @@ import { MapPickerComponent } from './map-picker.component';
imports: [
CommonModule,
FormsModule,
LeafletModule
LeafletModule,
TranslationModule
],
exports: [
MapPickerComponent

View File

@ -0,0 +1,69 @@
<div class="modal-header">
<h4 class="modal-title pull-left">Stato dell'allerta</h4>
<button type="button" class="btn-close close pull-right" [attr.aria-label]="'close'|translate|titlecase" (click)="bsModalRef.hide()">
<span aria-hidden="true" class="visually-hidden">&times;</span>
</button>
</div>
<div class="modal-body" *ngIf="id == 0">
<div class="d-flex justify-content-center mt-2 pt-2 mb-3">
<div class="spinner spinner-border"></div>
</div>
</div>
<div class="modal-body" *ngIf="id !== 0">
<table class="table table-border table-striped w-100">
<thead>
<tr>
<td>Nome</td>
<td colspan="2">Stato risposta</td>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let user of users">
<tr *ngIf="alertEnabled || user.response !== 'waiting'">
<td>
<img alt="red helmet" src="./assets/icons/red_helmet.png" width="20px" *ngIf="user.chief">
<img alt="red helmet" src="./assets/icons/black_helmet.png" width="20px" *ngIf="!user.chief">
{{ user.name }}
<img alt="driver" src="./assets/icons/wheel.png" width="20px" *ngIf="user.driver">
</td>
<ng-container *ngIf="user.response == 'waiting'">
<td style="width: 1px;"><i class="fas fa-spinner fa-spin"></i></td>
<td>In attesa di risposta</td>
</ng-container>
<ng-container *ngIf="user.response == true">
<td style="width: 1px;"><i class="fa fa-check" style="color:green"></i></td>
<td>Presente</td>
</ng-container>
<ng-container *ngIf="user.response == false">
<td style="width: 1px;"><i class="fa fa-times" style="color:red"></i></td>
<td>Non presente</td>
</ng-container>
</tr>
</ng-container>
</tbody>
</table>
<ng-container *ngIf="auth.profile.hasRole('SUPER_EDITOR') && alertEnabled">
<button type="button" class="btn btn-primary mb-2" (click)="isAdvancedCollapsed = !isAdvancedCollapsed"
[attr.aria-expanded]="!isAdvancedCollapsed" aria-controls="collapseBasic">
<ng-container *ngIf="isAdvancedCollapsed">Mostra impostazioni avanzate</ng-container>
<ng-container *ngIf="!isAdvancedCollapsed">Nascondi impostazioni avanzate</ng-container>
</button>
<div [collapse]="isAdvancedCollapsed" [isAnimated]="true">
<div class="well well-lg card card-block card-header">
<label for="details" class="form-label">Dettagli allerta</label>
<textarea class="form-control" id="details" rows="3" [(ngModel)]="notes"></textarea>
<button class="btn btn-secondary mt-2" (click)="saveAlertSettings()">Salva</button>
</div>
</div>
</ng-container>
<ng-container *ngIf="(!auth.profile.hasRole('SUPER_EDITOR') && notes !== '') || !alertEnabled">
<div class="well well-lg card card-block card-header">
<h5>Dettagli allerta</h5>
<h2>{{ notes }}</h2>
</div>
</ng-container>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" (click)="deleteAlert()" *ngIf="auth.profile.hasRole('SUPER_EDITOR') && alertEnabled">Rimuovi allerta corrente <i class="fas fa-exclamation-triangle"></i></button>
<button type="button" class="btn btn-secondary" (click)="bsModalRef.hide()">{{ 'close'|translate }}</button>
</div>

View File

@ -0,0 +1,101 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { BsModalRef } from 'ngx-bootstrap/modal';
import { ApiClientService } from 'src/app/_services/api-client.service';
import { AuthService } from 'src/app/_services/auth.service';
import { ToastrService } from 'ngx-toastr';
import Swal from 'sweetalert2';
const isEqual = (...objects: any[]) => objects.every(obj => JSON.stringify(obj) === JSON.stringify(objects[0]));
@Component({
selector: 'modal-alert',
templateUrl: './modal-alert.component.html',
styleUrls: ['./modal-alert.component.scss']
})
export class ModalAlertComponent implements OnInit, OnDestroy {
id = 0;
users: any[] = [];
isAdvancedCollapsed = true;
loadDataInterval: NodeJS.Timer | undefined = undefined;
notes = "";
alertEnabled = true;
constructor(
public bsModalRef: BsModalRef,
private api: ApiClientService,
public auth: AuthService,
private toastr: ToastrService
) { }
loadResponsesData() {
this.api.get(`alerts/${this.id}`).then((response) => {
if(this.alertEnabled !== response.enabled) this.alertEnabled = response.enabled;
if(!isEqual(this.users, response.crew)) this.users = response.crew;
if (this.notes === "" || this.notes === null) {
if(!isEqual(this.notes, response.notes)) this.notes = response.notes;
}
});
}
ngOnInit() {
this.loadDataInterval = setInterval(() => {
if (typeof (window as any).skipTableReload !== 'undefined' && (window as any).skipTableReload) {
return;
}
console.log("Refreshing responses data...");
this.loadResponsesData();
}, 2000);
this.loadResponsesData();
}
ngOnDestroy() {
if (this.loadDataInterval) {
console.log("Clearing interval...");
clearInterval(this.loadDataInterval);
}
}
saveAlertSettings() {
if(!this.auth.profile.hasRole('SUPER_EDITOR')) return;
this.api.post(`alerts/${this.id}/settings`, {
notes: this.notes
}).then((response) => {
this.toastr.success("Impostazioni salvate con successo");
});
}
deleteAlert() {
if(!this.auth.profile.hasRole('SUPER_EDITOR')) return;
Swal.fire({
title: "Sei sicuro di voler ritirare l'allarme?",
text: "I vigili verranno avvisati dell'azione",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: "Si, rimuovi",
cancelButtonText: "Annulla"
}).then((result: any) => {
if (result.isConfirmed) {
this.api.delete(`alerts/${this.id}`).then((response) => {
console.log(response);
this.bsModalRef.hide();
this.api.alertsChanged.next();
/*
this.translate.get('table.service_deleted_successfully').subscribe((res: string) => {
this.toastr.success(res);
});
this.loadTableData();
}).catch((e) => {
this.translate.get('table.service_deleted_error').subscribe((res: string) => {
this.toastr.error(res);
*/
});
}
});
}
}

View File

@ -1,6 +1,6 @@
<div class="modal-header">
<h4 class="modal-title pull-left">Modifica orari disponibilità</h4>
<button type="button" class="btn-close close pull-right" aria-label="Close" (click)="bsModalRef.hide()">
<h4 class="modal-title pull-left">{{ 'update_availability_schedule'|translate }}</h4>
<button type="button" class="btn-close close pull-right" [attr.aria-label]="'close'|translate|titlecase" (click)="bsModalRef.hide()">
<span aria-hidden="true" class="visually-hidden">&times;</span>
</button>
</div>
@ -11,7 +11,7 @@
<td style="background-color: white;"></td>
<ng-container *ngIf="orientation === 'portrait'">
<ng-container *ngFor="let day of days; let i = index">
<td class="day" (click)="selectDay(i)">{{ day.short }}</td>
<td class="day" (click)="selectDay(i)">{{ day.short|translate }}</td>
</ng-container>
</ng-container>
<ng-container *ngIf="orientation === 'landscape'">
@ -34,7 +34,7 @@
<tbody id="scheduler_body" *ngIf="orientation === 'landscape'">
<ng-container *ngFor="let day of days; let i = index">
<tr>
<td class="day" (click)="selectDay(i)">{{ day.short }}</td>
<td class="day" (click)="selectDay(i)">{{ day.short|translate }}</td>
<ng-container *ngFor="let hour of hours">
<td class="hour-cell" [class.highlighted] = "isCellSelected(i, hour)" (mousedown)="mouseDownCell(i, hour)" (mouseup)="mouseUpCell()" (mouseover)="mouseOverCell(i, hour)"></td>
</ng-container>
@ -44,6 +44,6 @@
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" (click)="saveChanges()">Salva le modifiche</button>
<button type="button" class="btn btn-secondary" (click)="bsModalRef.hide()">Chiudi</button>
<button type="button" class="btn btn-primary" (click)="saveChanges()">{{ 'save_changes'|translate }}</button>
<button type="button" class="btn btn-secondary" (click)="bsModalRef.hide()">{{ 'close'|translate }}</button>
</div>

View File

@ -13,32 +13,32 @@ export class ModalAvailabilityScheduleComponent implements OnInit {
public days = [
{
name: 'Lunedì',
short: 'Lun'
name: 'monday',
short: 'monday_short'
},
{
name: 'Martedì',
short: 'Mar'
name: 'tuesday',
short: 'tuesday_short'
},
{
name: 'Mercoledì',
short: 'Mer'
name: 'wednesday',
short: 'wednesday_short'
},
{
name: 'Giovedì',
short: 'Gio'
name: 'thursday',
short: 'thursday_short'
},
{
name: 'Venerdì',
short: 'Ven'
name: 'friday',
short: 'friday_short'
},
{
name: 'Sabato',
short: 'Sab'
name: 'saturday',
short: 'saturday_short'
},
{
name: 'Domenica',
short: 'Dom'
name: 'sunday',
short: 'sunday_short'
}
];
public hours = [

View File

@ -2,19 +2,20 @@
<table *ngIf="sourceType === 'list'" id="table" class="table table-striped table-bordered dt-responsive nowrap">
<thead>
<tr>
<th>Nome</th>
<th>Disponibile</th>
<th>Autista</th>
<ng-container *ngIf="auth.profile.full_viewer">
<th>Chiama</th>
<th>{{ 'name'|translate|titlecase }}</th>
<th>{{ 'available'|translate|titlecase }}</th>
<th>{{ 'driver'|translate|titlecase }}</th>
<ng-container *ngIf="auth.profile.hasRole('SUPER_EDITOR')">
<th>{{ 'call'|translate|titlecase }}</th>
</ng-container>
<th>Interventi</th>
<th>Minuti disponibilità</th>
<th>{{ 'services'|translate|titlecase }}</th>
<th>{{ 'availability_minutes'|translate|titlecase }}</th>
</tr>
</thead>
<tbody id="table_body">
<tr *ngFor="let row of data">
<td>
<i *ngIf="auth.profile.hasRole('SUPER_ADMIN') && row.id !== auth.profile.auth_user_id" class="fa fa-user me-2" (click)="onUserImpersonate(row.id)"></i>
<img alt="red helmet" src="./assets/icons/red_helmet.png" width="20px" *ngIf="row.chief">
<img alt="red helmet" src="./assets/icons/black_helmet.png" width="20px" *ngIf="!row.chief">
<ng-container *ngIf="(getTime() - row.online_time) < 30"><u>{{ row.name }}</u></ng-container>
@ -27,7 +28,7 @@
<td>
<img alt="driver" src="./assets/icons/wheel.png" width="20px" *ngIf="row.driver">
</td>
<td *ngIf="auth.profile.full_viewer">
<td *ngIf="auth.profile.hasRole('SUPER_EDITOR')">
<a href="tel:{{row.phone_number}}"><i class="fa fa-phone"></i></a>
</td>
<td>{{ row.services }}</td>
@ -38,10 +39,10 @@
<table *ngIf="sourceType === 'logs'" id="table" class="table table-striped table-bordered dt-responsive nowrap">
<thead>
<tr>
<th>Azione</th>
<th>Interessato</th>
<th>Fatto da</th>
<th>Data e ora</th>
<th>{{ 'action'|translate|titlecase }}</th>
<th>{{ 'changed'|translate|titlecase }}</th>
<th>{{ 'editor'|translate|titlecase }}</th>
<th>{{ 'datetime'|translate|titlecase }}</th>
</tr>
</thead>
<tbody id="table_body">
@ -56,31 +57,36 @@
<table *ngIf="sourceType === 'services'" id="table" class="table table-striped table-bordered dt-responsive nowrap">
<thead>
<tr>
<th>Inizio</th>
<th>Fine</th>
<th>Codice</th>
<th>Caposquadra</th>
<th>Autisti</th>
<th>Altre persone</th>
<th>Luogo</th>
<th>Note</th>
<th>Tipo</th>
<th hidden>Modifica</th>
<th>Rimuovi</th>
<th>#</th>
<th>{{ 'start'|translate|titlecase }}</th>
<th>{{ 'end'|translate|titlecase }}</th>
<th>{{ 'code'|translate|titlecase }}</th>
<th>{{ 'chief'|translate|titlecase }}</th>
<th>{{ 'drivers'|translate|titlecase }}</th>
<th>{{ 'crew'|translate|titlecase }}</th>
<th>{{ 'place'|translate|titlecase }}</th>
<th>{{ 'notes'|translate|titlecase }}</th>
<th>{{ 'type'|translate|titlecase }}</th>
<th>{{ 'update'|translate|titlecase }}</th>
<th>{{ 'remove'|translate|titlecase }}</th>
</tr>
</thead>
<tbody id="table_body">
<tr *ngFor="let row of data">
<tr *ngFor="let row of data; index as i">
<td>{{ data.length - i }}</td>
<td>{{ row.start | date:'dd/MM/YYYY, HH:mm' }}</td>
<td>{{ row.end | date:'dd/MM/YYYY, HH:mm' }}</td>
<td>{{ row.code }}</td>
<td>{{ row.chief }}</td>
<td>{{ row.drivers }}</td>
<td>{{ row.crew }}</td>
<td>{{ row.place_name }}</td>
<td>
{{ row.place_name }}<br>
<a class="place_details_link cursor-pointer" (click)="openPlaceDetails(row.lat, row.lng)">{{ 'more details'|translate|titlecase }}</a>
</td>
<td>{{ row.notes }}</td>
<td>{{ row.type }}</td>
<td hidden><i class="fa fa-edit"></i></td>
<td (click)="editService(row.id)"><i class="fa fa-edit"></i></td>
<td (click)="deleteService(row.id)"><i class="fa fa-trash"></i></td>
</tr>
</tbody>
@ -88,21 +94,21 @@
<table *ngIf="sourceType === 'trainings'" id="table" class="table table-striped table-bordered dt-responsive nowrap">
<thead>
<tr>
<th>Data</th>
<th>Nome</th>
<th>Tempo inizio</th>
<th>Tempo fine</th>
<th>Caposquadra</th>
<th>Altre persone</th>
<th>Luogo</th>
<th>Note</th>
<th hidden>Modifica</th>
<th hidden>Rimuovi</th>
<td>#</td>
<th>{{ 'name'|translate|titlecase }}</th>
<th>{{ 'start'|translate|titlecase }}</th>
<th>{{ 'end'|translate|titlecase }}</th>
<th>{{ 'chief'|translate|titlecase }}</th>
<th>{{ 'crew'|translate|titlecase }}</th>
<th>{{ 'place'|translate|titlecase }}</th>
<th>{{ 'notes'|translate|titlecase }}</th>
<th>{{ 'update'|translate|titlecase }}</th>
<th>{{ 'remove'|translate|titlecase }}</th>
</tr>
</thead>
<tbody id="table_body">
<tr *ngFor="let row of data">
<td>{{ row.date | date: 'MM/dd/yyyy HH:mm' }}</td>
<tr *ngFor="let row of data; index as i">
<td>{{ data.length - i }}</td>
<td>{{ row.name }}</td>
<td>{{ row.beginning }}</td>
<td>{{ row.end }}</td>
@ -110,8 +116,8 @@
<td>{{ row.crew }}</td>
<td>{{ row.place }}</td>
<td>{{ row.notes }}</td>
<td hidden><i class="fa fa-edit"></i></td>
<td hidden><i class="fa fa-trash"></i></td>
<td><i class="fa fa-edit"></i></td>
<td><i class="fa fa-trash"></i></td>
</tr>
</tbody>
</table>

View File

@ -27,3 +27,8 @@ img {
.cursor-pointer {
cursor: pointer;
}
.place_details_link {
text-decoration: underline;
color: #0d6efd;
}

View File

@ -3,6 +3,7 @@ import { Router } from '@angular/router';
import { ApiClientService } from 'src/app/_services/api-client.service';
import { AuthService } from '../../_services/auth.service';
import { ToastrService } from 'ngx-toastr';
import { TranslateService } from '@ngx-translate/core';
import Swal from 'sweetalert2';
@Component({
@ -16,6 +17,7 @@ export class TableComponent implements OnInit, OnDestroy {
@Input() refreshInterval?: number;
@Output() changeAvailability: EventEmitter<{user: number, newState: 0|1}> = new EventEmitter<{user: number, newState: 0|1}>();
@Output() userImpersonate: EventEmitter<number> = new EventEmitter<number>();
public data: any = [];
@ -25,7 +27,8 @@ export class TableComponent implements OnInit, OnDestroy {
private api: ApiClientService,
public auth: AuthService,
private router: Router,
private toastr: ToastrService
private toastr: ToastrService,
private translate: TranslateService
) { }
getTime() {
@ -33,12 +36,13 @@ export class TableComponent implements OnInit, OnDestroy {
}
loadTableData() {
this.api.get(this.sourceType || "list").then((data: any) => {
if(!this.sourceType) this.sourceType = "list";
this.api.get(this.sourceType).then((data: any) => {
console.log(data);
this.data = data.filter((row: any) => {
if(typeof row.hidden !== 'undefined') return !row.hidden;
return true;
});
this.data = data.filter((row: any) => typeof row.hidden !== 'undefined' ? !row.hidden : true);
if(this.sourceType === 'list') {
this.api.availableUsers = this.data.filter((row: any) => row.available).length;
}
});
}
@ -52,6 +56,9 @@ export class TableComponent implements OnInit, OnDestroy {
console.log("Refreshing data...");
this.loadTableData();
}, this.refreshInterval || 10000);
this.auth.authChanged.subscribe({
next: () => this.loadTableData()
});
}
ngOnDestroy(): void {
@ -61,35 +68,55 @@ export class TableComponent implements OnInit, OnDestroy {
}
onChangeAvailability(user: number, newState: 0|1) {
if(this.auth.profile.full_viewer) {
if(this.auth.profile.hasRole('SUPER_EDITOR')) {
this.changeAvailability.emit({user, newState});
}
}
onUserImpersonate(user: number) {
if(this.auth.profile.hasRole('SUPER_ADMIN')) {
this.auth.impersonate(user).then((user_id) => {
this.loadTableData();
this.userImpersonate.emit(user_id);
});
}
}
openPlaceDetails(lat: number, lng: number) {
this.router.navigate(['/place-details', lat, lng]);
}
editService(id: number) {
this.router.navigate(['/services', id]);
}
deleteService(id: number) {
console.log(id);
Swal.fire({
title: 'Sei del tutto sicuro di voler rimuovere l\'intervento?',
text: "Gli interventi eliminati non si possono recuperare.",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Si, rimuovilo',
cancelButtonText: 'Annulla'
}).then((result) => {
if (result.isConfirmed) {
this.api.delete(`services/${id}`).then((response) => {
this.toastr.success('Intervento rimosso con successo.');
this.loadTableData();
}).catch((e) => {
this.toastr.error('Errore durante la rimozione dell\'intervento.');
});
}
})
this.translate.get(['table.yes_remove', 'table.cancel', 'table.remove_service_confirm', 'table.remove_service_text']).subscribe((res: { [key: string]: string; }) => {
console.log(res);
Swal.fire({
title: res['table.remove_service_confirm'],
text: res['table.remove_service_confirm_text'],
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: res['table.yes_remove'],
cancelButtonText: res['table.cancel']
}).then((result) => {
if (result.isConfirmed) {
this.api.delete(`services/${id}`).then((response) => {
this.translate.get('table.service_deleted_successfully').subscribe((res: string) => {
this.toastr.success(res);
});
this.loadTableData();
}).catch((e) => {
this.translate.get('table.service_deleted_error').subscribe((res: string) => {
this.toastr.error(res);
});
});
}
});
});
}
}

View File

@ -1,5 +0,0 @@
<owner-image></owner-image>
<div class="text-center mb-4">
<button type="button" class="btn btn-primary" disabled>Aggiungi esercitazione</button>
</div>
<app-table [sourceType]="'trainings'" [refreshInterval]="1200000"></app-table>

View File

@ -0,0 +1,72 @@
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpHandler, HttpRequest, HttpEvent, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { AuthService } from '../_services/auth.service';
import { BehaviorSubject, Observable, throwError } from 'rxjs';
import { catchError, filter, switchMap, take } from 'rxjs/operators';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
private isRefreshing = false;
private refreshTokenSubject: BehaviorSubject<string|undefined> = new BehaviorSubject<string|undefined>(undefined);
constructor(private auth: AuthService) { }
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<Object>> {
const token = this.auth.getToken();
let authReq = this.addHeaders(req, token);
return next.handle(authReq).pipe(catchError(error => {
if (error instanceof HttpErrorResponse && !authReq.url.includes('login')) {
if(error.status === 400) {
return this.handle400Error(authReq, next);
} else if (error.status === 401) {
this.auth.logout();
}
}
return throwError(() => new Error(error));
}));
}
private handle400Error(request: HttpRequest<any>, next: HttpHandler) {
if (!this.isRefreshing) {
this.isRefreshing = true;
this.refreshTokenSubject.next(undefined);
return this.auth.refreshToken().pipe(
switchMap((token: string) => {
this.isRefreshing = false;
this.refreshTokenSubject.next(token);
return next.handle(this.addHeaders(request, token));
}),
catchError((err) => {
this.isRefreshing = false;
this.auth.logout();
return throwError(() => new Error(err));
})
);
}
return this.refreshTokenSubject.pipe(
filter(token => token !== undefined),
take(1),
switchMap((token) => {
return next.handle(this.addHeaders(request, token));
})
);
}
private addHeaders(request: HttpRequest<any>, token: string|undefined) {
if (typeof token === 'string' && token.length > 10) {
const headers = new HttpHeaders({
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Bearer ${token}`
});
return request.clone({ headers });
} else {
const headers = new HttpHeaders({
'Content-Type': 'application/x-www-form-urlencoded'
});
return request.clone({ headers });
}
}
}

View File

@ -1,23 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http';
import { Observable, tap } from 'rxjs';
import { AuthService } from '../_services/auth.service';
@Injectable()
export class UnauthorizedInterceptor implements HttpInterceptor {
constructor(private auth: AuthService) { }
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request).pipe( tap({
next: () => {},
error: (err: any) => {
if (err instanceof HttpErrorResponse) {
if (err.status !== 401 || request.url.includes('/login')) {
return;
}
console.log("Login required");
this.auth.logout();
}
}}));
}
}

View File

@ -3,29 +3,29 @@
<form method="post" [formGroup]="serviceForm" (ngSubmit)="formSubmit()">
<div class="container">
<div class="form-group has-validation">
<label for="date-picker">Inizio</label>
<label for="date-picker">{{ 'start'|translate|titlecase }}</label>
<datetime-picker formControlName="start" [class.is-invalid]="!isFieldValid('start')"></datetime-picker>
<div class="invalid-feedback" *ngIf="start.errors?.['required']">
Seleziona data e ora di inizio dell'intervento
<div class="invalid-feedback" *ngIf="start.errors?.['required']" translate>
edit_service.select_start_datetime
</div>
</div>
<div class="form-group has-validation">
<label for="date-picker">Fine</label>
<label for="date-picker">{{ 'end'|translate|titlecase }}</label>
<datetime-picker formControlName="end" [class.is-invalid]="!isFieldValid('end')"></datetime-picker>
<div class="invalid-feedback" *ngIf="end.errors?.['required']">
Seleziona data e ora di fine dell'intervento
<div class="invalid-feedback" *ngIf="end.errors?.['required']" translate>
edit_service.select_end_datetime
</div>
</div>
<div class="form-group has-validation">
<label for="progressivo">Progressivo</label>
<input formControlName="code" [class.is-invalid]="!isFieldValid('code')" id="progressivo" class="form-control"
<label for="code">{{ 'code'|translate|titlecase }}</label>
<input formControlName="code" [class.is-invalid]="!isFieldValid('code')" id="code" class="form-control"
type="text" placeholder="1234/5">
<div class="invalid-feedback" *ngIf="code.errors?.['required']">
Inserisci il progressivo dell'intervento
<div class="invalid-feedback" *ngIf="code.errors?.['required']" translate>
edit_service.insert_code
</div>
</div>
<div class="form-group has-validation" [class.is-invalid-div]="!isFieldValid('chief')">
<label>Caposquadra</label>
<label>{{ 'chief'|translate|titlecase }}</label>
<br>
<ng-container *ngFor="let user of users">
<div class="form-check">
@ -38,7 +38,7 @@
</ng-container>
</div>
<div class="form-group has-validation" [class.is-invalid-div]="!isFieldValid('drivers')">
<label>Autisti</label>
<label>{{ 'drivers'|translate|titlecase }}</label>
<br>
<ng-container *ngFor="let user of users">
<div class="form-check" *ngIf="user.driver">
@ -51,7 +51,7 @@
</ng-container>
</div>
<div class="form-group has-validation" [class.is-invalid-div]="!isFieldValid('crew')">
<label>Altri membri della squadra</label>
<label translate>edit_service.other_crew_members</label>
<br>
<ng-container *ngFor="let user of users">
<div class="form-check">
@ -64,38 +64,39 @@
</ng-container>
</div>
<div [class.is-invalid-div]="!isFieldValid('place')" class="mb-2">
<label>Luogo dell'intervento</label>
<map-picker (onMarkerSet)="setPlace($event.lat, $event.lng)"></map-picker>
<label>{{ 'place'|translate|titlecase }}</label>
<map-picker *ngIf="addingService" (onMarkerSet)="setPlace($event.lat, $event.lng)"></map-picker>
<map-picker *ngIf="!addingService && loadedServiceLat !== ''" (onMarkerSet)="setPlace($event.lat, $event.lng)" [selectLat]="loadedServiceLat" [selectLng]="loadedServiceLng"></map-picker>
</div>
<div class="form-group">
<label for="notes">Note (es. altre informazioni)</label><br>
<label for="notes">{{ 'notes'|translate|titlecase }}</label><br>
<textarea formControlName="notes" class="form-control" id="notes"></textarea>
</div>
<br>
<div class="form-group">
<label>Tipologia</label>
<label>{{ 'type'|translate|titlecase }}</label>
<br>
<div class="input-group has-validation">
<select formControlName="type" [class.is-invalid]="!isFieldValid('type')" class="form-control mr-2">
<option selected disabled>Seleziona tipologia..</option>
<option selected disabled translate>edit_service.select_type</option>
<option *ngFor="let service_type of types" value="{{ service_type.id }}">{{ service_type.name }}</option>
</select>
<button class="btn btn-outline-secondary" type="button" tabindex="-1" (click)="addingType = true">
Aggiungi
{{ 'add'|translate|titlecase }}
</button>
<div class="invalid-feedback" *ngIf="type.errors?.['required']">
Seleziona una tipologia di intervento
edit_service.select_service_type
</div>
</div>
<div class="input-group mb-2 mt-2" *ngIf="addingType">
<input type="text" class="form-control" placeholder="Nome della tipologia" [(ngModel)]="newType"
<input type="text" class="form-control" [placeholder]="'type'|translate" [(ngModel)]="newType"
[ngModelOptions]="{standalone: true}">
<button class="btn btn-secondary" type="button" (click)="addType()">Invia</button>
<button class="btn btn-secondary" type="button" (click)="addType()">{{ 'chief'|translate|titlecase }}</button>
</div>
</div>
<br>
<button id="submit_button" type="submit" class="btn btn-primary" [disabled]="submittingForm">Invia</button>
<button class="btn" type="button" (click)="formReset()" [disabled]="submittingForm">Reset</button>
<button id="submit_button" type="submit" class="btn btn-primary" [disabled]="submittingForm">{{ 'submit'|translate|titlecase }}</button>
<button class="btn" type="button" (click)="formReset()" [disabled]="submittingForm">{{ 'reset'|translate|titlecase }}</button>
<div class="d-flex justify-content-center mt-2 pt-2 mb-3" *ngIf="submittingForm">
<div class="spinner spinner-border"></div>
</div>

View File

@ -3,6 +3,7 @@ import { ActivatedRoute } from '@angular/router';
import { FormBuilder, Validators } from '@angular/forms';
import { ApiClientService } from 'src/app/_services/api-client.service';
import { ToastrService } from 'ngx-toastr';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'app-edit-service',
@ -23,6 +24,8 @@ export class EditServiceComponent implements OnInit {
notes: '',
type: ''
};
loadedServiceLat = "";
loadedServiceLng = "";
users: any[] = [];
types: any[] = [];
@ -64,7 +67,8 @@ export class EditServiceComponent implements OnInit {
private route: ActivatedRoute,
private api: ApiClientService,
private toastr: ToastrService,
private fb: FormBuilder
private fb: FormBuilder,
private translate: TranslateService
) {
this.route.paramMap.subscribe(params => {
this.serviceId = params.get('id') || undefined;
@ -73,6 +77,8 @@ export class EditServiceComponent implements OnInit {
} else {
this.api.get(`services/${this.serviceId}`).then((service) => {
this.loadedService = service;
this.loadedServiceLat = service.lat;
this.loadedServiceLng = service.lng;
let patch = Object.assign({}, service);
patch.start = new Date(parseInt(patch.start));
@ -100,11 +106,15 @@ export class EditServiceComponent implements OnInit {
addType() {
if(this.newType.length < 2) {
this.toastr.error("Il nome della tipologia deve essere lungo almeno 2 caratteri");
this.translate.get('edit_service.type_must_be_two_characters_long').subscribe((res: string) => {
this.toastr.error(res);
});
return;
}
if(this.types.find(t => t.name == this.newType)) {
this.toastr.error("Il nome della tipologia è già in uso");
this.translate.get('edit_service.type_already_exists').subscribe((res: string) => {
this.toastr.error(res);
});
return;
}
this.api.post("service_types", {
@ -113,8 +123,12 @@ export class EditServiceComponent implements OnInit {
this.addingType = false;
this.newType = "";
console.log(type);
if(type === 1) this.toastr.success("Tipologia di servizio aggiunta con successo.");
this.loadTypes();
if(type == 1) {
this.translate.get('edit_service.type_added_successfully').subscribe((res: string) => {
this.toastr.success(res);
});
this.loadTypes();
}
});
}
@ -165,11 +179,15 @@ export class EditServiceComponent implements OnInit {
console.log(values);
this.api.post("services", values).then((res) => {
console.log(res);
this.toastr.success("Intervento aggiunto con successo.");
this.translate.get('edit_service.service_added_successfully').subscribe((res: string) => {
this.toastr.success(res);
});
this.submittingForm = false;
}).catch((err) => {
console.error(err);
this.toastr.error("Errore durante l'aggiunta dell'intervento");
this.translate.get('edit_service.service_add_failed').subscribe((res: string) => {
this.toastr.error(res);
});
this.submittingForm = false;
});
}

View File

@ -3,9 +3,10 @@ import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
import { MapPickerModule } from '../map-picker/map-picker.module';
import { DatetimePickerModule } from '../datetime-picker/datetime-picker.module';
import { BackBtnModule } from '../back-btn/back-btn.module';
import { MapPickerModule } from '../../_components/map-picker/map-picker.module';
import { DatetimePickerModule } from '../../_components/datetime-picker/datetime-picker.module';
import { BackBtnModule } from '../../_components/back-btn/back-btn.module';
import { TranslationModule } from '../../translation.module';
import { EditServiceRoutingModule } from './edit-service-routing.module';
import { EditServiceComponent } from './edit-service.component';
@ -22,7 +23,8 @@ import { EditServiceComponent } from './edit-service.component';
BsDatepickerModule.forRoot(),
MapPickerModule,
DatetimePickerModule,
BackBtnModule
BackBtnModule,
TranslationModule
]
})
export class EditServiceModule { }

View File

@ -0,0 +1,34 @@
<div class="text-center">
<h3 *ngIf="available !== undefined">{{ 'list.your_availability_is'|translate }} <b>{{ available ? ("available"|translate|uppercase) : ("unavailable"|translate|uppercase) }}{{ manual_mode ? "" : " ("+('programmed'|translate)+")" }}</b></h3>
<div id="availability-btn-group">
<button (click)="changeAvailibility(1)" type="button" [delay]="1000" [tooltip]="'tooltip_change_availability'|translate:{state: 'available'|translate}" id="activate-btn" class="btn btn-lg btn-success me-1">{{ 'set_available'|translate|titlecase }}</button>
<button (click)="changeAvailibility(0)" type="button" [delay]="1000" [tooltip]="'tooltip_change_availability'|translate:{state: 'unavailable'|translate}" id="deactivate-btn" class="btn btn-lg btn-danger">{{ 'set_unavailable'|translate|titlecase }}</button>
</div>
<ng-container *ngIf="manual_mode !== undefined">
<button type="button" class="btn btn-secondary" *ngIf="manual_mode" (click)="updateManualMode(0)">
{{ 'list.enable_schedules'|translate }}
</button>
<button type="button" class="btn btn-secondary" *ngIf="!manual_mode" (click)="updateManualMode(1)">
{{ 'list.disable_schedules'|translate }}
</button>
<br>
</ng-container>
<button type="button" class="btn btn-lg" (click)="openScheduleModal()">
{{ 'list.update_schedules'|translate }}
</button>
</div>
<owner-image></owner-image>
<div class="text-center" *ngIf="auth.profile.hasRole('SUPER_EDITOR')">
<div class="btn-group" role="group">
<button type="button" class="btn btn-danger" (click)="addAlertFull()" [disabled]="!api?.availableUsers || api.availableUsers! < 5 || alertLoading">
🚒 Richiedi squadra completa
</button>
<button type="button" class="btn btn-warning" (click)="addAlertSupport()" [disabled]="!api?.availableUsers || api.availableUsers! < 2 || alertLoading">
Richiedi squadra di supporto 🧯
</button>
</div>
</div>
<app-table [sourceType]="'list'" (changeAvailability)="changeAvailibility($event.newState, $event.user)" #table></app-table>
<div class="text-center">
<button (click)="requestTelegramToken()" class="btn btn-md btn-success mt-3">{{ 'list.connect_telegram_bot'|translate }}</button>
</div>

View File

@ -1,9 +1,11 @@
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { TableComponent } from '../table/table.component';
import { ModalAvailabilityScheduleComponent } from '../modal-availability-schedule/modal-availability-schedule.component';
import { TableComponent } from '../../_components/table/table.component';
import { ModalAvailabilityScheduleComponent } from '../../_components/modal-availability-schedule/modal-availability-schedule.component';
import { ModalAlertComponent } from 'src/app/_components/modal-alert/modal-alert.component';
import { ApiClientService } from 'src/app/_services/api-client.service';
import { ToastrService } from 'ngx-toastr';
import { BsModalService, BsModalRef } from 'ngx-bootstrap/modal';
import { TranslateService } from '@ngx-translate/core';
import { AuthService } from 'src/app/_services/auth.service';
@Component({
@ -11,8 +13,9 @@ import { AuthService } from 'src/app/_services/auth.service';
templateUrl: './list.component.html',
styleUrls: ['./list.component.scss']
})
export class ListComponent implements OnInit {
export class ListComponent implements OnInit, OnDestroy {
scheduleModalRef?: BsModalRef;
alertModalRef?: BsModalRef;
@ViewChild('table') table!: TableComponent;
public loadAvailabilityInterval: NodeJS.Timer | undefined = undefined;
@ -20,11 +23,14 @@ export class ListComponent implements OnInit {
public available: boolean | undefined = undefined;
public manual_mode: boolean | undefined = undefined;
public alertLoading = false;
constructor(
private api: ApiClientService,
private auth: AuthService,
public api: ApiClientService,
public auth: AuthService,
private toastr: ToastrService,
private modalService: BsModalService
private modalService: BsModalService,
private translate: TranslateService
) {
this.loadAvailability();
}
@ -33,11 +39,13 @@ export class ListComponent implements OnInit {
this.api.get("availability").then((response) => {
this.available = response.available;
this.manual_mode = response.manual_mode;
console.log(this.available, this.manual_mode);
});
}
changeAvailibility(available: 0|1, id?: number|undefined) {
if(typeof id === 'undefined') {
id = this.auth.profile.auth_user_id;
}
this.api.post("availability", {
id: id,
available: available
@ -54,7 +62,9 @@ export class ListComponent implements OnInit {
this.api.post("manual_mode", {
manual_mode: manual_mode
}).then((response) => {
this.toastr.success("Modalità manuale aggiornata con successo.");
this.translate.get('list.manual_mode_updated_successfully').subscribe((res: string) => {
this.toastr.success(res);
});
this.loadAvailability();
});
}
@ -63,11 +73,58 @@ export class ListComponent implements OnInit {
this.scheduleModalRef = this.modalService.show(ModalAvailabilityScheduleComponent, Object.assign({}, { class: 'modal-custom' }));
}
addAlertFull() {
this.alertLoading = true;
if(!this.auth.profile.hasRole('SUPER_EDITOR')) return;
this.api.post("alerts", {
type: "full"
}).then((response) => {
this.alertLoading = false;
if(response?.status === "error") {
this.toastr.error(response.message, undefined, {
timeOut: 5000
});
return;
}
this.alertModalRef = this.modalService.show(ModalAlertComponent, {
initialState: {
id: response.id
}
});
this.api.alertsChanged.next();
});
}
addAlertSupport() {
this.alertLoading = true;
if(!this.auth.profile.hasRole('SUPER_EDITOR')) return;
this.api.post("alerts", {
type: "support"
}).then((response) => {
this.alertLoading = false;
if(response?.status === "error") {
this.toastr.error(response.message, undefined, {
timeOut: 5000
});
return;
}
this.alertModalRef = this.modalService.show(ModalAlertComponent, {
initialState: {
id: response.id
}
});
this.api.alertsChanged.next();
});
}
ngOnInit(): void {
this.loadAvailabilityInterval = setInterval(() => {
console.log("Refreshing availability...");
this.loadAvailability();
}, 10000);
this.auth.authChanged.subscribe({
next: () => this.loadAvailability()
});
}
ngOnDestroy(): void {

View File

@ -3,15 +3,15 @@
<owner-image></owner-image>
<div class="my-2 text-danger" *ngIf="!loginResponse.loginOk">{{ loginResponse.message }}</div>
<div class="form-floating">
<input type="text" class="form-control" (keydown.enter)="inputPassword.focus()" [(ngModel)]="username" id="username" placeholder="Username">
<label for="username">Username</label>
<input type="text" class="form-control" (keydown.enter)="inputPassword.focus()" [(ngModel)]="username" id="username" [placeholder]="'username'|translate">
<label for="username" translate>login.username</label>
</div>
<div class="form-floating">
<input type="password" class="form-control" (keydown.enter)="login()" [(ngModel)]="password" id="password" placeholder="Password" #inputPassword>
<label for="password">Password</label>
<input type="password" class="form-control" (keydown.enter)="login()" [(ngModel)]="password" id="password" [placeholder]="'password'|translate" #inputPassword>
<label for="password" translate>login.password</label>
</div>
<button class="w-100 btn btn-lg btn-primary" (click)="login()" [disabled]="loading">
<span *ngIf="!loading">Login</span>
<span *ngIf="!loading" translate>login.submit_btn</span>
<div class="spinner-border spinner-border-sm text-white" *ngIf="loading"></div>
</button>
</main>

View File

@ -8,28 +8,28 @@
</div>
<div class="place_info" *ngIf="place_loaded">
<h3>
<a href="https://www.google.com/maps/@?api=1&map_action=map&center={{ lat }},{{ lng }}&zoom=19&basemap=satellite" target="_blank">Apri il luogo in Google Maps</a>
<a href="https://www.google.com/maps/@?api=1&map_action=map&center={{ lat }},{{ lng }}&zoom=19&basemap=satellite" target="_blank">{{ 'place_details.open_in_google_maps'|translate }}</a>
</h3>
<br>
<h4 *ngIf="place_info.place_name">
Nome: <b>{{ place_info.place_name }}</b>
{{ 'name'|translate|titlecase }}: <b>{{ place_info.place_name }}</b>
</h4>
<h4 *ngIf="place_info.building_service_name">
Nome del luogo: <b>{{ place_info.building_service_name }}</b>
{{ 'place_details.place_name'|translate|titlecase }}: <b>{{ place_info.building_service_name }}</b>
</h4>
<h4 *ngIf="place_info.house_number">
Numero civico: <b>{{ place_info.house_number }}</b>
{{ 'place_details.house_number'|translate|titlecase }}: <b>{{ place_info.house_number }}</b>
</h4>
<h4 *ngIf="place_info.road">
Strada: <b>{{ place_info.road }}</b>
{{ 'place_details.road'|translate|titlecase }}: <b>{{ place_info.road }}</b>
</h4>
<h4 *ngIf="place_info.village">
Comune: <b>{{ place_info.village }}</b> (CAP <b>{{ place_info.postcode }}</b>)
{{ 'place_details.village'|translate|titlecase }}: <b>{{ place_info.village }}</b> ({{ 'place_details.postcode'|translate }} <b>{{ place_info.postcode }}</b>)
</h4>
<h4 *ngIf="place_info.hamlet">
Frazione: <b>{{ place_info.hamlet }}</b>
{{ 'place_details.hamlet'|translate|titlecase }}: <b>{{ place_info.hamlet }}</b>
</h4>
<h4 *ngIf="place_info.municipality">
Raggruppamento del Comune: <b>{{ place_info.municipality }}</b>
{{ 'place_details.municipality'|translate|titlecase }}: <b>{{ place_info.municipality }}</b>
</h4>
</div>

View File

@ -1,8 +1,9 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TranslationModule } from '../../translation.module';
import { LeafletModule } from '@asymmetrik/ngx-leaflet';
import { BackBtnModule } from '../back-btn/back-btn.module';
import { BackBtnModule } from '../../_components/back-btn/back-btn.module';
import { PlaceDetailsRoutingModule } from './place-details-routing.module';
import { PlaceDetailsComponent } from './place-details.component';
@ -15,7 +16,8 @@ import { PlaceDetailsComponent } from './place-details.component';
CommonModule,
PlaceDetailsRoutingModule,
LeafletModule,
BackBtnModule
BackBtnModule,
TranslationModule
]
})
export class PlaceDetailsModule { }

View File

@ -1,5 +1,5 @@
<owner-image></owner-image>
<div class="text-center mb-4">
<button type="button" class="btn btn-primary" (click)="addService()">Aggiungi intervento</button>
<button type="button" class="btn btn-primary" (click)="addService()">{{ 'add'|translate|titlecase }} {{ 'service'|translate }}</button>
</div>
<app-table [sourceType]="'services'" [refreshInterval]="1200000"></app-table>

View File

@ -0,0 +1,5 @@
<owner-image></owner-image>
<div class="text-center mb-4">
<button type="button" class="btn btn-primary" disabled>{{ 'add'|translate|titlecase }} {{ 'training'|translate }}</button>
</div>
<app-table [sourceType]="'trainings'" [refreshInterval]="1200000"></app-table>

View File

@ -1,29 +1,17 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Subject } from "rxjs";
@Injectable({
providedIn: 'root'
})
export class ApiClientService {
private apiRoot = 'api/';
public requestOptions = {};
constructor(private http: HttpClient) {
this.requestOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/x-www-form-urlencoded'
})
}
}
public alertsChanged = new Subject<void>();
public availableUsers: undefined | number = undefined;
public setToken(token: string) {
this.requestOptions = {
headers: new HttpHeaders({
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/x-www-form-urlencoded'
})
}
}
constructor(private http: HttpClient) { }
public apiEndpoint(endpoint: string): string {
if(endpoint.startsWith('https')) {
@ -45,42 +33,37 @@ export class ApiClientService {
public get(endpoint: string, data: any = {}) {
return new Promise<any>((resolve, reject) => {
this.http.get(this.apiEndpoint(endpoint), {
...this.requestOptions,
params: new HttpParams({ fromObject: data })
}).subscribe((data: any) => {
resolve(data);
}, (err) => {
reject(err);
}).subscribe({
next: (v) => resolve(v),
error: (e) => reject(e)
});
});
}
public post(endpoint: string, data: any) {
public post(endpoint: string, data: any = {}) {
return new Promise<any>((resolve, reject) => {
this.http.post(this.apiEndpoint(endpoint), this.dataToParams(data), this.requestOptions).subscribe((data: any) => {
resolve(data);
}, (err) => {
reject(err);
this.http.post(this.apiEndpoint(endpoint), this.dataToParams(data)).subscribe({
next: (v) => resolve(v),
error: (e) => reject(e)
});
});
}
public put(endpoint: string, data: any) {
public put(endpoint: string, data: any = {}) {
return new Promise<any>((resolve, reject) => {
this.http.put(this.apiEndpoint(endpoint), this.dataToParams(data), this.requestOptions).subscribe((data: any) => {
resolve(data);
}, (err) => {
reject(err);
this.http.put(this.apiEndpoint(endpoint), this.dataToParams(data)).subscribe({
next: (v) => resolve(v),
error: (e) => reject(e)
});
});
}
public delete(endpoint: string) {
return new Promise<any>((resolve, reject) => {
this.http.delete(this.apiEndpoint(endpoint), this.requestOptions).subscribe((data: any) => {
resolve(data);
}, (err) => {
reject(err);
this.http.delete(this.apiEndpoint(endpoint)).subscribe({
next: (v) => resolve(v),
error: (e) => reject(e)
});
});
}

View File

@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { ApiClientService } from './api-client.service';
import { Observable, Subject } from "rxjs";
import jwt_decode from 'jwt-decode';
export interface LoginResponse {
@ -13,13 +14,15 @@ export interface LoginResponse {
})
export class AuthService {
public profile: any = undefined;
private access_token = '';
private access_token: string | undefined = undefined;
public authChanged = new Subject<void>();
public loadProfile() {
try{
console.log("Loading profile", this.access_token);
let now = Date.now().valueOf() / 1000;
(window as any).jwt_decode = jwt_decode;
if(typeof(this.access_token) !== "string") return;
let decoded: any = jwt_decode(this.access_token);
if (typeof decoded.exp !== 'undefined' && decoded.exp < now) {
return false;
@ -29,7 +32,12 @@ export class AuthService {
}
this.profile = decoded.user_info;
this.profile.hasRole = (role: string) => {
return Object.values(this.profile.roles).includes(role);
}
console.log(this.profile);
this.authChanged.next();
return true;
} catch(e) {
console.error(e);
@ -42,19 +50,22 @@ export class AuthService {
constructor(private api: ApiClientService, private router: Router) {
if(localStorage.getItem("access_token") !== null) {
this.access_token = localStorage.getItem("access_token") as string;
this.api.setToken(this.access_token);
this.loadProfile();
}
}
private setToken(value: string) {
public setToken(value: string) {
localStorage.setItem("access_token", value);
this.access_token = value;
this.api.setToken(this.access_token);
this.loadProfile();
}
public getToken(): string | undefined {
return this.access_token;
}
private removeToken() {
this.access_token = '';
localStorage.removeItem("access_token");
}
@ -97,12 +108,54 @@ export class AuthService {
})
}
public impersonate(user_id: number): Promise<number> {
return new Promise((resolve, reject) => {
console.log("final", user_id);
this.api.post("impersonate", {
user_id: user_id
}).then((response) => {
this.setToken(response.access_token);
resolve(user_id);
}).catch((err) => {
reject();
});
});
}
public stop_impersonating(): Promise<number> {
return new Promise((resolve, reject) => {
this.api.post("stop_impersonating").then((response) => {
this.setToken(response.access_token);
resolve(response.user_id);
}).catch((err) => {
reject();
});
});
}
public logout(routerDestination?: string[] | undefined) {
this.removeToken();
this.profile = undefined;
if(routerDestination === undefined) {
routerDestination = ["login", "list"];
if(this.profile.impersonating_user) {
this.stop_impersonating().then((user_id) => {
});
} else {
this.removeToken();
this.profile = undefined;
if(routerDestination === undefined) {
routerDestination = ["login", "list"];
}
this.router.navigate(routerDestination);
}
this.router.navigate(routerDestination);
}
public refreshToken() {
return new Observable<string>((observer) => {
this.api.post("refreshToken").then((data: any) => {
this.setToken(data.token);
observer.next(data.token);
observer.complete();
}).catch((err) => {
observer.error(err);
});
});
}
}

View File

@ -1,13 +1,13 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ListComponent } from './_components/list/list.component';
import { LogsComponent } from './_components/logs/logs.component';
import { ServicesComponent } from './_components/services/services.component';
import { TrainingsComponent } from './_components/trainings/trainings.component';
import { ListComponent } from './_routes/list/list.component';
import { LogsComponent } from './_routes/logs/logs.component';
import { ServicesComponent } from './_routes/services/services.component';
import { TrainingsComponent } from './_routes/trainings/trainings.component';
import { AuthorizeGuard } from './_guards/authorize.guard';
import { LoginComponent } from './_components/login/login.component';
import { LoginComponent } from './_routes/login/login.component';
const routes: Routes = [
{ path: 'list', component: ListComponent, canActivate: [AuthorizeGuard] },
@ -15,12 +15,12 @@ const routes: Routes = [
{ path: 'services', component: ServicesComponent, canActivate: [AuthorizeGuard] },
{
path: 'place-details',
loadChildren: () => import('./_components/place-details/place-details.module').then(m => m.PlaceDetailsModule),
loadChildren: () => import('./_routes/place-details/place-details.module').then(m => m.PlaceDetailsModule),
canActivate: [AuthorizeGuard]
},
{
path: 'services/:id',
loadChildren: () => import('./_components/edit-service/edit-service.module').then(m => m.EditServiceModule),
loadChildren: () => import('./_routes/edit-service/edit-service.module').then(m => m.EditServiceModule),
canActivate: [AuthorizeGuard]
},
{ path: 'trainings', component: TrainingsComponent, canActivate: [AuthorizeGuard] },

View File

@ -1,19 +1,36 @@
<div [className]="menuButtonClicked ? 'topnav responsive' : 'topnav'" id="topNavBar" *ngIf="auth.profile !== undefined">
<a routerLinkActive="active" (click)="menuButtonClicked = false" routerLink="/list">Lista disponibilità</a>
<a routerLinkActive="active" (click)="menuButtonClicked = false" routerLink="/services">Interventi</a>
<a routerLinkActive="active" (click)="menuButtonClicked = false" routerLink="/trainings">Esercitazioni</a>
<a routerLinkActive="active" (click)="menuButtonClicked = false" routerLink="/logs">Logs</a>
<a style="float: right;" id="logout">Ciao, {{ auth.profile.name }}. <b id="logout-text" (click)="auth.logout()">Logout</b></a>
<a routerLinkActive="active" (click)="menuButtonClicked = false" routerLink="/list" translate>menu.list</a>
<a routerLinkActive="active" (click)="menuButtonClicked = false" routerLink="/services" translate>menu.services</a>
<a routerLinkActive="active" (click)="menuButtonClicked = false" routerLink="/trainings" translate>menu.trainings</a>
<a routerLinkActive="active" (click)="menuButtonClicked = false" routerLink="/logs" translate>menu.logs</a>
<a style="float: right;" id="logout">{{ 'menu.hi'|translate|titlecase }}, {{ auth.profile.name }}. <b id="logout-text" (click)="auth.logout()" translate>menu.logout</b></a>
<a class="icon" id="menuButton" (click)="menuButtonClicked = !menuButtonClicked"></a>
</div>
<div class="d-flex justify-content-center mt-4 pt-4 mb-3" *ngIf="loadingRoute">
<div class="spinner spinner-border"></div>
</div>
<div class="container">
<div class="d-flex justify-content-center mt-4 pt-4 mb-3" *ngIf="loadingRoute">
<div class="spinner spinner-border"></div>
</div>
<router-outlet></router-outlet>
<alert type="danger" *ngIf="alerts.length > 0">
<strong>Attenzione!</strong> Allertamento in corso.<br>
<ng-container *ngIf="alerts.length == 1">
Emergenza attuale: <a (click)="openAlert(alerts[0]['id'])"><b>{{ alerts[0]["created_at"] | date:'dd/MM/YYYY, HH:mm:ss' }}</b> (premere per ulteriori informazioni)</a>
</ng-container>
<ng-container *ngIf="alerts.length > 1">
Emergenze attuali:
<ul>
<li *ngFor="let alert of alerts">
<a (click)="openAlert(alert['id'])"><b>{{ alert["created_at"] | date:'dd/MM/YYYY, HH:mm:ss' }}</b> (premere per ulteriori informazioni)</a>
</li>
</ul>
</ng-container>
</alert>
<router-outlet></router-outlet>
</div>
<div id="footer" class="footer text-center p-3">
Allerta-VVF, software libero realizzato per i Vigili del Fuoco volontari.<br>
<p>Revisione {{ versions.revision }} ({{ revision_datetime_string }})</p>
{{ 'footer_text' | translate }}<br>
<p>{{ 'revision' | translate | titlecase }} {{ versions.revision }} ({{ revision_datetime_string }})</p>
</div>

View File

@ -3,6 +3,9 @@ import { AuthService } from './_services/auth.service';
import { LocationBackService } from 'src/app/_services/locationBack.service';
import { versions } from 'src/environments/versions';
import { Router, RouteConfigLoadStart, RouteConfigLoadEnd } from '@angular/router';
import { ApiClientService } from './_services/api-client.service';
import { ModalAlertComponent } from 'src/app/_components/modal-alert/modal-alert.component';
import { BsModalService } from 'ngx-bootstrap/modal';
@Component({
selector: 'app-root',
@ -14,15 +17,27 @@ export class AppComponent {
public revision_datetime_string;
public versions = versions;
public loadingRoute = false;
private loadAlertsInterval: NodeJS.Timer | undefined = undefined;
public alerts = [];
constructor(
public auth: AuthService,
private locationBackService: LocationBackService,
private router: Router
private router: Router,
private api: ApiClientService,
private modalService: BsModalService
) {
this.revision_datetime_string = new Date(versions.revision_timestamp).toLocaleString(undefined, { day: '2-digit', month: '2-digit', year: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' });
}
loadAlerts() {
if(this.auth.profile) {
this.api.get("alerts").then((response) => {
this.alerts = response;
});
}
}
ngOnInit () {
this.router.events.subscribe((event) => {
if (event instanceof RouteConfigLoadStart) {
@ -31,5 +46,23 @@ export class AppComponent {
this.loadingRoute = false;
}
});
this.loadAlertsInterval = setInterval(() => {
console.log("Refreshing alerts...");
this.loadAlerts();
}, 15000);
this.loadAlerts();
this.api.alertsChanged.subscribe(() => {
this.loadAlerts();
});
}
openAlert(id: number) {
this.modalService.show(ModalAlertComponent, {
initialState: {
id: id
}
});
}
}

View File

@ -1,11 +1,16 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { HttpClient, HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { TranslationModule } from './translation.module';
import { ToastrModule } from 'ngx-toastr';
import { ModalModule } from 'ngx-bootstrap/modal';
import { TooltipModule } from 'ngx-bootstrap/tooltip';
import { CollapseModule } from 'ngx-bootstrap/collapse';
import { AlertModule } from 'ngx-bootstrap/alert';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@ -14,16 +19,17 @@ import { environment } from '../environments/environment';
import { TableComponent } from './_components/table/table.component';
import { ModalAvailabilityScheduleComponent } from './_components/modal-availability-schedule/modal-availability-schedule.component';
import { ModalAlertComponent } from './_components/modal-alert/modal-alert.component';
import { OwnerImageComponent } from './_components/owner-image/owner-image.component';
import { LoginComponent } from './_components/login/login.component';
import { LoginComponent } from './_routes/login/login.component';
import { ListComponent } from './_components/list/list.component';
import { LogsComponent } from './_components/logs/logs.component';
import { ServicesComponent } from './_components/services/services.component';
import { TrainingsComponent } from './_components/trainings/trainings.component';
import { ListComponent } from './_routes/list/list.component';
import { LogsComponent } from './_routes/logs/logs.component';
import { ServicesComponent } from './_routes/services/services.component';
import { TrainingsComponent } from './_routes/trainings/trainings.component';
import { UnauthorizedInterceptor } from './_providers/unauthorized-interceptor.provider';
import { AuthInterceptor } from './_providers/auth-interceptor.provider';
@NgModule({
declarations: [
@ -31,6 +37,7 @@ import { UnauthorizedInterceptor } from './_providers/unauthorized-interceptor.p
//
TableComponent,
ModalAvailabilityScheduleComponent,
ModalAlertComponent,
OwnerImageComponent,
//
LoginComponent,
@ -54,18 +61,32 @@ import { UnauthorizedInterceptor } from './_providers/unauthorized-interceptor.p
}),
ModalModule.forRoot(),
TooltipModule.forRoot(),
CollapseModule.forRoot(),
AlertModule.forRoot(),
ServiceWorkerModule.register('ngsw-worker.js', {
enabled: false && environment.production,
// Register the ServiceWorker as soon as the app is stable
// or after 30 seconds (whichever comes first).
registrationStrategy: 'registerWhenStable:30000'
})
}),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
}),
TranslationModule
],
providers: [{
provide: HTTP_INTERCEPTORS,
useClass: UnauthorizedInterceptor,
useClass: AuthInterceptor,
multi: true
}],
bootstrap: [AppComponent]
})
export class AppModule { }
export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}

View File

@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
@NgModule({
exports: [
CommonModule,
TranslateModule
]
})
export class TranslationModule {
constructor(private translate: TranslateService) {
this.translate.setDefaultLang('en');
this.translate.use(window.navigator.language.split("-")[0]);
}
}

View File

@ -0,0 +1,115 @@
{
"menu": {
"list": "List",
"services": "Services",
"trainings": "Trainings",
"logs": "Logs",
"logout": "Logout",
"hi": "hi"
},
"table": {
"yes_remove": "Yes, remove",
"cancel": "Cancel",
"remove_service_confirm": "Are you sure you want to remove this service?",
"remove_service_confirm_text": "This action cannot be undone.",
"service_deleted_successfully": "Service deleted successfully",
"service_deleted_error": "Service could not be deleted. Please try again."
},
"list": {
"your_availability_is": "You are:",
"enable_schedules": "Enable hour schedules",
"disable_schedules": "Disable hour schedules",
"update_schedules": "Update availability schedules",
"connect_telegram_bot": "Connect your account to the Telegram bot",
"tooltip_change_availability": "Change your availability to {{state}}",
"manual_mode_updated_successfully": "Manual mode updated successfully"
},
"login": {
"username": "username",
"password": "password",
"submit_btn": "Login"
},
"place_details": {
"open_in_google_maps": "Open in Google Maps",
"place_name": "Place name",
"house_number": "house number",
"road": "road",
"village": "village",
"postcode": "postcode",
"hamlet": "hamlet",
"municipality": "municipality"
},
"map_picker": {
"place_min_length": "Place name must be at least 3 characters long",
"loading_error": "Error loading search results. Please try again later"
},
"edit_service": {
"select_start_datetime": "Select start date and time for the service",
"select_end_datetime": "Select end date and time for the service",
"insert_code": "Insert service code",
"other_crew_members": "Other crew members",
"select_type": "Select a type",
"select_service_type": "Select a service type",
"type_must_be_two_characters_long": "Type must be at least 2 characters long",
"type_already_exists": "Type already exists",
"type_added_successfully": "Type added successfully",
"service_added_successfully": "Service added successfully",
"service_add_error": "Service could not be added. Please try again"
},
"update_availability_schedule": "Update availability schedule",
"save_changes": "Save changes",
"close": "Close",
"monday": "Monday",
"monday_short": "Mon",
"tuesday": "Tuesday",
"tuesday_short": "Tue",
"wednesday": "Wednesday",
"wednesday_short": "Wed",
"thursday": "Thursday",
"thursday_short": "Thu",
"friday": "Friday",
"friday_short": "Fri",
"saturday": "Saturday",
"saturday_short": "Sat",
"sunday": "Sunday",
"sunday_short": "Sun",
"programmed": "programmed",
"available": "available",
"unavailable": "unavailable",
"set_available": "available",
"set_unavailable": "unavailable",
"name": "name",
"driver": "driver",
"drivers": "drivers",
"call": "call",
"service": "service",
"services": "services",
"training": "training",
"trainings": "trainings",
"user": "user",
"users": "users",
"availability_minutes": "availability_minutes",
"action": "action",
"changed": "changed",
"editor": "editor",
"datetime": "datetime",
"start": "start",
"end": "end",
"code": "code",
"chief": "chief",
"crew": "crew",
"place": "place",
"notes": "notes",
"type": "type",
"add": "add",
"update": "update",
"remove": "remove",
"more details": "more details",
"search": "search",
"submit": "invia",
"reset": "reset",
"go_back": "go back",
"press_to_select_a_date": "press to select a date",
"footer_text": "Allerta-VVF, free software developed for volunteer firefighters brigades.",
"revision": "revision"
}

View File

@ -0,0 +1,115 @@
{
"menu": {
"list": "Lista disponibilità",
"services": "Interventi",
"trainings": "Esercitazioni",
"logs": "Logs",
"logout": "Logout",
"hi": "Ciao"
},
"table": {
"yes_remove": "Si, rimuovi",
"cancel": "Annulla",
"remove_service_confirm": "Sei sicuro di voler rimuovere questo intervento?",
"remove_service_confirm_text": "Questa operazione non può essere annullata.",
"service_deleted_successfully": "Intervento rimosso con successo",
"service_deleted_error": "Errore durante la rimozione dell'intervento. Riprova più tardi"
},
"list": {
"your_availability_is": "Attualmente sei:",
"enable_schedules": "Abilita programmazione oraria",
"disable_schedules": "Disattiva programmazione oraria",
"update_schedules": "Modifica orari disponibilità",
"connect_telegram_bot": "Collega l'account al bot Telegram",
"tooltip_change_availability": "Cambia la tua disponibilità in {{state}}",
"manual_mode_updated_successfully": "Modalità manuale aggiornata con successo"
},
"login": {
"username": "username",
"password": "password",
"submit_btn": "Login"
},
"place_details": {
"open_in_google_maps": "Apri il luogo in Google Maps",
"place_name": "Nome del luogo",
"house_number": "numero civico",
"road": "strada",
"village": "comune",
"postcode": "CAP",
"hamlet": "frazione",
"municipality": "raggruppamento del comune"
},
"map_picker": {
"place_min_length": "Il nome della località deve essere di almeno 3 caratteri",
"loading_error": "Errore di caricamento dei risultati della ricerca. Riprovare più tardi"
},
"edit_service": {
"select_start_datetime": "Seleziona data e ora di inizio dell'intervento",
"select_end_datetime": "Seleziona data e ora di fine dell'intervento",
"insert_code": "Inserisci il progressivo dell'intervento",
"other_crew_members": "Altri membri della squadra",
"select_type": "Seleziona una tipologia",
"select_service_type": "Seleziona una tipologia di intervento",
"type_must_be_two_characters_long": "La tipologia deve essere di almeno 2 caratteri",
"type_already_exists": "La tipologia è già presente",
"type_added_successfully": "Tipologia aggiunta con successo",
"service_added_successfully": "Intervento aggiunto con successo",
"service_add_error": "Errore durante l'aggiunta dell'intervento. Riprovare più tardi"
},
"update_availability_schedule": "Aggiorna programmazione disponibilità",
"save_changes": "Salva modifiche",
"close": "Chiudi",
"monday": "Lunedì",
"monday_short": "Lun",
"tuesday": "Martedì",
"tuesday_short": "Mar",
"wednesday": "Mercoledì",
"wednesday_short": "Mer",
"thursday": "Giovedì",
"thursday_short": "Gio",
"friday": "Venerdì",
"friday_short": "Ven",
"saturday": "Sabato",
"saturday_short": "Sab",
"sunday": "Domenica",
"sunday_short": "Dom",
"programmed": "programmata",
"available": "disponibile",
"unavailable": "non disponibile",
"set_available": "attiva",
"set_unavailable": "disattiva",
"name": "nome",
"driver": "autista",
"drivers": "autisti",
"call": "chiama",
"service": "intervento",
"services": "interventi",
"training": "esercitazione",
"trainings": "esercitazioni",
"user": "utente",
"users": "utenti",
"availability_minutes": "minuti di disponibilità",
"action": "azione",
"changed": "interessato",
"editor": "fatto da",
"datetime": "data e ora",
"start": "inizio",
"end": "fine",
"code": "codice",
"chief": "caposquadra",
"crew": "squadra",
"place": "luogo",
"notes": "note",
"type": "tipologia",
"add": "aggiungi",
"update": "modifica",
"remove": "rimuovi",
"more details": "altri dettagli",
"search": "cerca",
"submit": "invia",
"reset": "reset",
"go_back": "torna indietro",
"press_to_select_a_date": "premi per selezionare una data",
"footer_text": "Allerta-VVF, software libero realizzato per i Vigili del Fuoco volontari.",
"revision": "revisione"
}

View File

@ -1,106 +0,0 @@
<IfModule mod_php4.c>
php_flag display_errors Off
</IfModule>
<IfModule mod_php5.c>
php_flag display_errors Off
</IfModule>
<IfModule mod_php7.c>
php_flag display_errors Off
</IfModule>
<IfModule mod_autoindex.c>
Options -Indexes
</IfModule>
<IfModule mod_headers.c>
Header unset X-Powered-By
Header unset X-Pingback
Header unset SERVER
</IfModule>
<IfModule mod_rewrite.c>
RewriteEngine on
RewriteCond %{REQUEST_METHOD} ^(TRACE|DELETE|TRACK) [NC]
RewriteRule ^(.*)$ - [F,L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule .* error_page.php?error=404 [L]
</IfModule>
<FilesMatch "(config|config.old|config-sample|core|ui)\.php">
Order Deny,Allow
Deny from all
</FilesMatch>
<FilesMatch "\.log$">
Order Allow,Deny
Deny from all
</FilesMatch>
<FilesMatch "^\.ht">
Order Allow,Deny
Deny from all
</FilesMatch>
<IfModule mod_mime.c>
# From https://htaccessbook.com/useful-htaccess-rules/
# JAVASCRIPT
AddType application/javascript js jsonp
AddType application/json json
<FilesMatch "manifest.webmanifest">
AddType application/manifest+json webmanifest
</FilesMatch>
# FONTS
AddType font/opentype otf
AddType application/font-woff woff
AddType application/x-font-woff woff
AddType application/vnd.ms-fontobject eot
AddType application/x-font-ttf ttc ttf
AddType image/svg+xml svg svgz
AddType application/wasm wasm.gz wasm
AddEncoding gzip gz
# AUDIO
AddType audio/mp4 m4a f4a f4b
AddType audio/ogg oga ogg
# VIDEO
AddType video/mp4 mp4 m4v f4v f4p
AddType video/ogg ogv
AddType video/webm webm
AddType video/x-flv flv
# OTHERS
AddType application/octet-stream safariextz
AddType application/x-chrome-extension crx
AddType application/x-opera-extension oex
AddType application/x-shockwave-flash swf
AddType application/x-web-app-manifest+json webapp
AddType application/x-xpinstall xpi
AddType application/xml atom rdf rss xml
AddType application/vnd.openxmlformats .docx .pptx .xlsx .xltx . xltm .dotx .potx .ppsx
AddType text/cache-manifest appcache manifest
AddType text/vtt vtt
AddType text/x-component htc
AddType text/x-vcard vcf
AddType image/webp webp
AddType image/x-icon ico
</IfModule>
<IfModule mod_headers.c>
<FilesMatch "\.(js|css|woff|woff2|ttf|eot)$">
Header set Cache-Control "max-age=2592000, public"
</FilesMatch>
<FilesMatch "\.(jpe?g|png|gif|swf|flv|pdf|svg|ico)$">
Header set Cache-Control "max-age=604800, public"
</FilesMatch>
</IfModule>
ServerSignature Off

View File

@ -1,277 +0,0 @@
<?php
define('REQUEST_USING_API', true);
require 'core.php';
use Spatie\ArrayToXml\ArrayToXml;
use Brick\PhoneNumber\PhoneNumber;
use Brick\PhoneNumber\PhoneNumberFormat;
use Brick\PhoneNumber\PhoneNumberParseException;
$user_info = [];
$dispatcher = FastRoute\simpleDispatcher(
function (FastRoute\RouteCollector $r) {
$r->addRoute(
'GET', '/healthcheck', function ($vars) {
return ["state" => "SUCCESS", "description" => ""];
}
);
$r->addRoute(
['GET', 'POST'], '/requestDebug', function ($vars) {
return ["get" => $_GET, "post" => $_POST, "server" => $_SERVER];
}
);
$r->addRoute(
'POST', '/login', function ($vars) {
global $tools, $db, $user;
try {
$user->auth->loginWithUsername($_POST['username'], $_POST['password']);
$apiKey = $tools->createKey();
$db->insert(
DB_PREFIX."_api_keys",
["apikey" => $apiKey, "user" => $user->auth->getUserId(), "permissions" => "all"]
);
return ["status" => "ok", "apiKey" => $apiKey];
}
catch (\Delight\Auth\UnknownUsernameException $e) {
http_response_code(401);
return ["status" => "error", "message" => "Username unknown"];
}
catch (\Delight\Auth\AmbiguousUsernameException $e) {
http_response_code(401);
return ["status" => "error", "message" => "Ambiguous Username"];
}
catch (\Delight\Auth\InvalidPasswordException $e) {
http_response_code(401);
return ["status" => "error", "message" => "Wrong password"];
}
catch (\Delight\Auth\EmailNotVerifiedException $e) {
http_response_code(401);
return ["status" => "error", "message" => "Email not verified"];
}
catch (\Delight\Auth\TooManyRequestsException $e) {
http_response_code(429);
return ["status" => "error", "message" => "Too many requests"];
}
}
);
$r->addRoute(
'GET', '/users', function ($vars) {
requireToken();
global $db;
$users = $db->select("SELECT * FROM `".DB_PREFIX."_users`");
$users_profiles = $db->select("SELECT * FROM `".DB_PREFIX."_profiles`");
foreach ($users_profiles as $key=>$value){
if(is_null($users_profiles[$key]["name"])) {
$users_profiles[$key]["name"] = $users[$key]["username"];
}
$users_profiles[$key]["email"] = $users[$key]["email"];
}
return $users_profiles;
}
);
$r->addRoute(
'GET', '/user', function ($vars) {
requireToken();
global $db, $user_info;
$users = $db->select("SELECT * FROM `".DB_PREFIX."_users` WHERE id = :id", ["id" => $user_info["id"]])[0];
$users_profiles = $db->select("SELECT * FROM `".DB_PREFIX."_profiles` WHERE id = :id", ["id" => $user_info["id"]])[0];
if(is_null($users_profiles["name"])) {
$users_profiles["name"] = $users["username"];
}
$users_profiles["email"] = $users["email"];
return $users_profiles;
}
);
$r->addRoute(
'GET', '/user/{id:\d+}', function ($vars) {
requireToken();
global $db;
$users = $db->select("SELECT * FROM `".DB_PREFIX."_users` WHERE id = :id", ["id" => $vars["id"]])[0];
$users_profiles = $db->select("SELECT * FROM `".DB_PREFIX."_profiles` WHERE id = :id", ["id" => $vars["id"]])[0];
if(is_null($users_profiles["name"])) {
$users_profiles["name"] = $users["username"];
}
$users_profiles["email"] = $users["email"];
return $users_profiles;
}
);
$r->addRoute(
'POST', '/user', function ($vars) {
requireToken();
global $user, $user_info;
$chief = isset($_POST["chief"]) ? $_POST["chief"]==1 : false;
$driver = isset($_POST["driver"]) ? $_POST["driver"]==1 : false;
$hidden = isset($_POST["hidden"]) ? $_POST["hidden"]==1 : false;
$disabled = isset($_POST["disabled"]) ? $_POST["disabled"]==1 : false;
if(isset($_POST["mail"], $_POST["name"], $_POST["username"], $_POST["password"], $_POST["phone_number"], $_POST["birthday"])) {
try {
$phone_number = PhoneNumber::parse($_POST["phone_number"]);
if (!$phone_number->isValidNumber()) {
return ["status" => "error", "message" => "Bad phone number"];
} else {
$phone_number = $phone_number->format(PhoneNumberFormat::E164);
}
} catch (PhoneNumberParseException $e) {
return ["status" => "error", "message" => "Bad phone number"];
}
try{
$userId = $user->add_user($_POST["mail"], $_POST["name"], $_POST["username"], $_POST["password"], $phone_number, $_POST["birthday"], $chief, $driver, $hidden, $disabled, $user_info["id"]);
} catch (\Delight\Auth\InvalidEmailException $e) {
return ["status" => "error", "message" => "Invalid email address"];
} catch (\Delight\Auth\InvalidPasswordException $e) {
return ["status" => "error", "message" => "Invalid password"];
} catch (\Delight\Auth\UserAlreadyExistsException $e) {
return ["status" => "error", "message" => "User already exists"];
}
if($userId) {
return ["userId" => $userId];
} else {
return ["status" => "error", "message" => "Unknown error"];
}
} else {
return ["status" => "error", "message" => "User info required"];
}
}
);
$r->addRoute(
'GET', '/availability', function ($vars) {
requireToken();
global $db, $user_info;
return $db->select("SELECT * FROM `".DB_PREFIX."_profiles` WHERE id = :id", ["id" => $user_info["id"]])[0]["available"];
}
);
$r->addRoute(
'GET', '/availability/{id:\d+}', function ($vars) {
requireToken();
global $db;
return $db->select("SELECT * FROM `".DB_PREFIX."_profiles` WHERE id = :id", ["id" => $vars["id"]])[0]["available"];
}
);
$r->addRoute(
'GET', '/changeAvailability/{available:\d+}', function ($vars) {
requireToken();
global $user, $db, $user_info;
$vars["available"] = (int) $vars["available"];
if($vars["available"] !== 0 && $vars["available"] !== 1) {
return ["status" => "error", "message" => "Availability code not allowed"];
}
$log_message = $vars["available"] ? "Status changed to 'available'" : "Status changed to 'not available'";
$db->select("UPDATE `".DB_PREFIX."_profiles` SET `available` = :available WHERE `id` = :id", ["id" => $user_info["id"], "available" => $vars["available"]]);
$user->log($log_message);
}
);
$r->addRoute(
'GET', '/changeAvailability/{id:\d+}/{available:\d+}', function ($vars) {
requireToken();
global $user, $db, $user_info;
$vars["available"] = (int) $vars["available"];
if($vars["available"] !== 0 && $vars["available"] !== 1) {
return ["status" => "error", "message" => "Availability code not allowed"];
}
$log_message = $vars["available"] ? "Status changed to 'available'" : "Status changed to 'not available'";
$db->select("UPDATE `".DB_PREFIX."_profiles` SET `available` = :available WHERE `id` = :id", ["id" => $vars["id"], "available" => $vars["available"]]);
$user->log($log_message, $vars["id"], $user_info["id"]);
}
);
}
);
$httpMethod = $_SERVER['REQUEST_METHOD'];
$uri = $_SERVER['REQUEST_URI'];
$uri = str_replace($_SERVER['SCRIPT_NAME'], "", $uri);
$uri = str_replace("///", "/", $uri);
$uri = str_replace("//", "/", $uri);
$uri = "/" . trim($uri, "/");
// Strip query string (?foo=bar) and decode URI
if (false !== $pos = strpos($uri, '?')) {
$uri = substr($uri, 0, $pos);
}
$uri = rawurldecode($uri);
// Get response format
if (isset($_GET["xml"])) {
$responseFormat = "xml";
$responseFormatType = "application/xml";
} else if (isset($_GET["json"])) {
$responseFormat = "json";
$responseFormatType = "application/json";
} else if (false !== strpos($uri, 'xml')) {
$responseFormat = "xml";
$responseFormatType = "application/xml";
$uri = str_replace(".xml", "", $uri);
} else if (false !== strpos($uri, 'json')) {
$responseFormat = "json";
$responseFormatType = "application/json";
$uri = str_replace(".json", "", $uri);
} else {
$responseFormat = "json";
$responseFormatType = "application/json";
}
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Headers: *");
header("Access-Control-Allow-Methods: *");
header("Access-Control-Max-Age: *");
header("Content-type: ".$responseFormatType);
init_class(false, false); //initialize classes after Content-type header
$routeInfo = $dispatcher->dispatch($httpMethod, $uri);
function responseApi($content, $status_code=200)
{
global $responseFormat;
if($status_code !== 200) {
http_response_code($status_code);
}
if($responseFormat == "json") {
echo(json_encode($content));
} else {
echo(ArrayToXml::convert($content));
}
}
function validToken()
{
global $db, $user_info;
$token = isset($_REQUEST['apiKey']) ? $_REQUEST['apiKey'] : (isset($_REQUEST['apikey']) ? $_REQUEST['apikey'] : (isset($_SERVER['HTTP_APIKEY']) ? $_SERVER['HTTP_APIKEY'] : false));
if($token == false) {
return false;
}
if(!empty($api_key_row = $db->select("SELECT * FROM `".DB_PREFIX."_api_keys` WHERE apikey = :apikey", ["apikey" => $token]))) {
$user_info["id"] = $db->select("SELECT * FROM `".DB_PREFIX."_profiles` WHERE id = :id", ["id" => $api_key_row[0]["user"]])[0]["id"];
return true;
} else {
return false;
}
}
function requireToken()
{
if(!validToken()) {
responseApi(["status" => "error", "message" => "Access Denied"], 401);
exit();
}
}
if($_SERVER['REQUEST_METHOD'] == "OPTIONS"){
exit();
}
switch ($routeInfo[0]) {
case FastRoute\Dispatcher::NOT_FOUND:
http_response_code(404);
responseApi(["status" => "error", "message" => "Route not found"]);
break;
case FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
$allowedMethods = $routeInfo[1];
http_response_code(405);
responseApi(["status" => "error", "message" => "Method not allowed", "usedMethod" => $_SERVER['REQUEST_METHOD']]);
break;
case FastRoute\Dispatcher::FOUND:
$handler = $routeInfo[1];
$vars = $routeInfo[2];
responseApi($handler($vars));
bdump($vars);
break;
}

View File

@ -1,4 +0,0 @@
<?php
require("../core.php");
init_class();
$tools->rickroll();

View File

@ -1,26 +0,0 @@
<?php
// ** Database settings ** //
/* The name of the database for Allerta-vvf */
define('DB_NAME', '@@db@@');
/* Database username */
define('DB_USER', '@@user@@');
/* Database password */
define('DB_PASSWORD', '@@password@@');
/* Database hostname */
define('DB_HOST', '@@host@@');
/* Database hostname */
define('DB_PREFIX', '@@prefix@@');
/* Telegram bot options */
define('BOT_TELEGRAM_API_KEY', '');
define('BOT_TELEGRAM_USERNAME', '');
/* Sentry options */
define('SENTRY_CSP_REPORT_URI', '');
define('SENTRY_ENABLED', false);
define('SENTRY_DSN', '');
define('SENTRY_ENV', 'prod');

View File

@ -1,17 +0,0 @@
<?php
header("Content-type: application/json");
try {
$start_time = microtime(true);
include "core.php";
init_class(false, false);
$exec_time = microtime(true) - $start_time;
$server_side = ["status" => "ok", "status_msg" => null, "exec_time" => $exec_time, "user_info" => $user->info()];
} catch (Exception $e) {
$server_side = ["status" => "error", "status_msg" => $e];
}
try {
$client_side = ["status" => "ok", "status_msg" => null, "ip" => $tools->get_ip()];
} catch (Exception $e) {
$server_side = ["status" => "error", "status_msg" => $e];
}
echo(json_encode(["client" => $client_side, "server" => $server_side]));

File diff suppressed because it is too large Load Diff

View File

@ -1,175 +0,0 @@
<?php
require_once 'core.php';
init_class(false);
error_reporting(-1);
list($cronJobDay, $cronJobTime) = explode(";", get_option("cron_job_time"));
$execDateTime = [
"day" => date("d"),
"month" => date("m"),
"year" => date("Y"),
"hour" => date("H"),
"minutes" => date("i")
];
$cronJobDateTime = [
"day" => $cronJobDay,
"month" => date("m"),
"year" => date("Y"),
"hour" => explode(":", $cronJobTime)[0],
"minutes" => explode(":", $cronJobTime)[1]
];
$start = get_option("cron_job_enabled") && ((isset($_POST['cron']) && $_POST['cron'] == "cron_job-".get_option("cron_job_code")) || (isset($_SERVER['HTTP_CRON']) && $_SERVER['HTTP_CRON'] == "cron_job-".get_option("cron_job_code")));
$start_reset = ( $execDateTime["day"] == $cronJobDateTime["day"] &&
$execDateTime["day"] == $cronJobDateTime["day"] &&
$execDateTime["month"] == $cronJobDateTime["month"] &&
$execDateTime["year"] == $cronJobDateTime["year"] &&
$execDateTime["hour"] == $cronJobDateTime["hour"] &&
$execDateTime["minutes"] - $cronJobDateTime["minutes"] < 5 );
$action = "Availability Minutes ";
if($start) {
if($start_reset) {
$action .= "reset and ";
$profiles = $db->select("SELECT * FROM `".DB_PREFIX."_profiles` WHERE `available` = 1 ");
if(count($profiles) > 0) {
$list = [];
foreach($profiles as $profile){
$list[] = [$profile["id"] => $profile["availability_minutes"]];
}
$db->insert(
DB_PREFIX."_minutes",
["month" => $execDateTime["month"], "year" => $execDateTime["year"], "list"=>json_encode($list)]
);
$db->exec("UPDATE `".DB_PREFIX."_profiles` SET `availability_minutes` = 0");
}
}
$action .= "update";
$profiles = $db->select("SELECT * FROM `".DB_PREFIX."_profiles` WHERE `available` = 1");
if(count($profiles) > 0) {
$output = [];
$output[] = $profiles;
$output_status = "ok";
$queries = [];
foreach ($profiles as $row) {
$value = (int)$row["availability_minutes"]+5;
$id = $row["id"];
$increment[$id] = $value;
$count = $db->update(
DB_PREFIX."_profiles",
["availability_minutes" => $value],
["id" => $id]
);
$tmp = $id . " - " . $value . " ";
$tmp .= $count == 1 ? "success" : "fail";
$queries[] = $tmp;
}
$output[] = $queries;
} else {
$output = ["profiles array empty"];
$output_status = "ok";
}
$result = $db->select("SELECT * FROM `".DB_PREFIX."_schedules`;");
$schedules_check = [];
$schedules_users = [];
$schedules_check["schedules"] = [];
$schedules_check["users"] = [];
if(!empty($result)){
foreach ($result as $key => $value) {
$result[$key]["schedules"] = json_decode($result[$key]["schedules"]);
}
$schedules_check["table"] = $result;
foreach ($result as $key => $row) {
$last_exec = $row["last_exec"];
$last_exec = [
"day" => (int) explode(";",$row["last_exec"])[0],
"hour" => (int) explode(":",explode(";",$row["last_exec"])[1])[0],
"minutes" => (int) explode(":",$row["last_exec"])[1]
];
$id = $row["id"];
$user_id = $row["user"];
$selected_holidays = json_decode($row["holidays"]);
$selected_holidays_dates = [];
foreach ($selected_holidays as $holiday){
$selected_holidays_dates[] = $user->holidays->getHoliday($holiday)->format('Y-m-d');
}
foreach ($row["schedules"] as $key => $value) {
$schedule = [
"day" => (int) $value[0]+1,
"hour" => (int) explode(":",$value[1])[0],
"minutes" => (int) explode(":",$value[1])[1]
];
$now = [
"day" => (int) date("N"),
"hour" => (int) date("H"),
"minutes" => (int) date("i")
];
if(
$schedule["day"] == $now["day"] &&
$schedule["hour"] == $now["hour"] &&
$schedule["minutes"] <= $now["minutes"] &&
$now["minutes"] - $schedule["minutes"] <= 30
){
if(!in_array($user_id,$schedules_users)) $schedules_users[] = $user_id;
if($schedule["hour"] == $last_exec["hour"] ? $schedule["minutes"] !== $last_exec["minutes"] : true && !in_array(date('Y-m-d'), $selected_holidays_dates)){
$last_exec_new = $schedule["day"].";".sprintf("%02d", $schedule["hour"]).":".sprintf("%02d", $schedule["minutes"]);
$db->update(
DB_PREFIX."_schedules",
["last_exec" => $last_exec_new],
["id" => $id]
);
$db->update(
DB_PREFIX."_profiles",
["available" => '1', "availability_last_change" => "cron"],
["id" => $user_id]
);
$schedules_check["schedules"][] = [
"schedule" => $schedule,
"now" => $now,
"exec" => $last_exec,
"last_exec_new" => $last_exec_new,
];
}
}
}
}
$schedules_check["users"] = $schedules_users;
$profiles = $db->select("SELECT id FROM `".DB_PREFIX."_profiles`");
foreach ($profiles as $profile) {
if(!in_array($profile["id"],$schedules_users)){
$db->update(
DB_PREFIX."_profiles",
["available" => 0],
["availability_last_change" => "cron", "id" => $profile["id"]]
);
}
}
}
} else {
require("error_page.php");
show_error_page(405, "Method not allowed", "Allowed methods: POST");
exit();
}
header('Content-Type: application/json');
echo(json_encode(
[
"start" => $start,
"start_reset" => $start_reset,
"execDateTime" => $execDateTime,
"cronJobDateTime" => $cronJobDateTime,
"action" => $action,
"schedules_check" => $schedules_check,
"output" => [
"status" => $output_status,
"message" => $output
]
]
));

View File

@ -1,11 +0,0 @@
{
"baseUrl": "http://127.0.0.1:8080/",
"projectId": "4ty9gy",
"env": {
"DB_PASSWORD": "password"
},
"experimentalStudio": true,
"chromeWebSecurity": false,
"defaultCommandTimeout": 40000,
"requestTimeout": 60000
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1 +0,0 @@
[{"place_id":257830288,"licence":"Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright","osm_type":"relation","osm_id":44882,"boundingbox":["45.2042549","46.3548531","9.8362928","10.842675"],"lat":"45.77958045","lon":"10.4258729694612","display_name":"Brescia, Lombardia, Italia","class":"boundary","type":"administrative","importance":0.7354640205264362,"icon":"https://nominatim.openstreetmap.org/ui/mapicons//poi_boundary_administrative.p.20.png"},{"place_id":257390216,"licence":"Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright","osm_type":"relation","osm_id":45144,"boundingbox":["45.4807171","45.5899752","10.1470684","10.3002186"],"lat":"45.5398022","lon":"10.2200214","display_name":"Brescia, Lombardia, Italia","class":"boundary","type":"administrative","importance":0.7271074461208031,"icon":"https://nominatim.openstreetmap.org/ui/mapicons//poi_boundary_administrative.p.20.png"},{"place_id":54063101,"licence":"Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright","osm_type":"node","osm_id":4553822557,"boundingbox":["45.5273342","45.5373342","10.2078404","10.2178404"],"lat":"45.5323342","lon":"10.2128404","display_name":"Brescia, Piazzale Stazione, Centro Storico Sud, Zona Centro, Brescia, Lombardia, 25122, Italia","class":"railway","type":"station","importance":0.48614554653236075,"icon":"https://nominatim.openstreetmap.org/ui/mapicons//transport_train_station2.p.20.png"},{"place_id":23143065,"licence":"Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright","osm_type":"node","osm_id":2409443276,"boundingbox":["44.4687976","44.4688976","8.6048681","8.6049681"],"lat":"44.4688476","lon":"8.6049181","display_name":"Brescia, Urbe, Savona, Liguria, 17048, Italia","class":"place","type":"isolated_dwelling","importance":0.29999999999999993},{"place_id":163311426,"licence":"Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright","osm_type":"way","osm_id":307938666,"boundingbox":["-29.9019023","-29.9008417","-71.2670192","-71.2669167"],"lat":"-29.9014479","lon":"-71.2669709","display_name":"Brescia, Condominio Capri, La Serena, Provincia de Elqui, Coquimbo Region, 17000000, Cile","class":"highway","type":"residential","importance":0.19999999999999998}]

View File

@ -1 +0,0 @@
[{"place_id":258244980,"licence":"Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright","osm_type":"relation","osm_id":44915,"boundingbox":["45.3867381","45.5358482","9.0408867","9.2781103"],"lat":"45.4668","lon":"9.1905","display_name":"Milano, Lombardia, Italia","class":"boundary","type":"administrative","importance":0.8390652078701348,"icon":"https://nominatim.openstreetmap.org/ui/mapicons//poi_boundary_administrative.p.20.png"},{"place_id":258789516,"licence":"Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright","osm_type":"relation","osm_id":344499,"boundingbox":["41.0766967","41.1206314","-6.6489375","-6.566364"],"lat":"41.0923837","lon":"-6.5984217","display_name":"El Milano, Salamanca, Castiglia e León, Spagna","class":"boundary","type":"administrative","importance":0.6058586680411897,"icon":"https://nominatim.openstreetmap.org/ui/mapicons//poi_boundary_administrative.p.20.png"},{"place_id":258556780,"licence":"Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright","osm_type":"relation","osm_id":44881,"boundingbox":["45.1614258","45.6434829","8.7060961","9.551457"],"lat":"45.454211900000004","lon":"9.111350961779705","display_name":"Milano, Lombardia, Italia","class":"boundary","type":"administrative","importance":0.6027406455537151,"icon":"https://nominatim.openstreetmap.org/ui/mapicons//poi_boundary_administrative.p.20.png"},{"place_id":259530334,"licence":"Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright","osm_type":"relation","osm_id":6605766,"boundingbox":["30.696897","30.718944","-96.881245","-96.841162"],"lat":"30.7106407","lon":"-96.864745","display_name":"Milano, Milam County, Texas, 76556, Stati Uniti d'America","class":"boundary","type":"administrative","importance":0.5332195319778348,"icon":"https://nominatim.openstreetmap.org/ui/mapicons//poi_boundary_administrative.p.20.png"},{"place_id":5770611,"licence":"Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright","osm_type":"node","osm_id":650721553,"boundingbox":["42.7623859","42.8023859","12.5780599","12.6180599"],"lat":"42.7823859","lon":"12.5980599","display_name":"Milano, Spoleto, Perugia, Umbria, 06049, Italia","class":"place","type":"hamlet","importance":0.5316717311890431,"icon":"https://nominatim.openstreetmap.org/ui/mapicons//poi_place_village.p.20.png"}]

View File

@ -1,81 +0,0 @@
[{
"email": "user1@mail.local",
"name": "Kathy Jordan",
"username": "kathy.jordan",
"password": "4jpbly8g6a",
"birthday": "08/08/1995",
"chief": false,
"driver": false
}, {
"email": "user2@mail.local",
"name": "Natalie Jordan",
"username": "natalie.jordan",
"password": "kf343di74s",
"birthday": "04/05/2011",
"chief": true,
"driver": false
}, {
"email": "user3@mail.local",
"name": "Keith Li",
"username": "keith.li",
"password": "3ojvgk4fpv",
"birthday": "11/04/2019",
"chief": true,
"driver": true
}, {
"email": "user4@mail.local",
"name": "Pellegrino Scotto",
"username": "pellegrino.scotto",
"password": "e0ou92taw3",
"birthday": "20/10/1983",
"chief": true,
"driver": false
}, {
"email": "user5@mail.local",
"name": "William Torres",
"username": "william.torres",
"password": "0fso8sxxe0",
"birthday": "16/07/2000",
"chief": false,
"driver": true
}, {
"email": "user6@mail.local",
"name": "Napoleone Tomasetti",
"username": "napoleone.tomasetti",
"password": "so7ykv8a7g",
"birthday": "27/12/1978",
"chief": true,
"driver": true
}, {
"email": "user7@mail.local",
"name": "Gelsomina Murray",
"username": "gelsomina.murray",
"password": "x1js0s6zao",
"birthday": "22/10/1994",
"chief": true,
"driver": true
}, {
"email": "user8@mail.local",
"name": "Letizia Petrucelli",
"username": "letizia.petrucelli",
"password": "k1hsbdt3cv",
"birthday": "24/04/1981",
"chief": true,
"driver": false
}, {
"email": "user9@mail.local",
"name": "Giampaolo Surian",
"username": "giampaolo.surian",
"password": "et52m65s4g",
"birthday": "10/06/1972",
"chief": true,
"driver": true
}, {
"email": "user10@mail.local",
"name": "Cassandra Jensen",
"username": "cassandra.jensen",
"password": "9h3fb37ccw",
"birthday": "28/10/1985",
"chief": true,
"driver": false
}]

View File

@ -1,116 +0,0 @@
describe("Installation", () => {
before(() => {
cy.exec("rm config.old.php", {failOnNonZeroExit: false});
cy.exec("mv config.php config.old.php", {failOnNonZeroExit: false});
cy.visit("/");
cy.get(".button").click();
})
beforeEach(() => {
cy.setCookie("forceLanguage", "en");
})
it('Write wrong DB pwd and user', function () {
cy.get("input[name='dbname']")
.clear()
.type("allerta_db_"+Date.now())
cy.get("input[name='uname']")
.clear()
.type("root_wrongpwd_"+Date.now())
cy.get("input[name='pwd']")
.clear()
.should('have.value', '')
cy.get(".button").click();
cy.contains("Error establishing a database connection");
cy.visit("/");
cy.get(".button").click();
})
it('Write correct DB pwd and user', function () {
cy.get("input[name='dbname']")
.clear()
.type("allerta_db_"+Date.now())
cy.get("input[name='uname']")
.clear()
.type("root")
.should('have.value', 'root')
cy.get("input[name='pwd']")
.clear()
.type(Cypress.env("DB_PASSWORD"))
.should('have.value', Cypress.env("DB_PASSWORD"))
cy.get(".button").click();
cy.contains("Great job, man!");
cy.get(".button").click();
})
it('Finish installation', function () {
cy.get("input[name='user_name']")
.clear()
.type("admin")
.should('have.value', 'admin')
cy.get("input[name='admin_password']")
.clear()
.type("password")
.should('have.value', 'password')
cy.get("#pass-strength-result")
.should('have.text', 'Very weak')
.should('have.class', 'short')
cy.get("input[name='admin_password']")
.clear()
.type("passsword")
.should('have.value', 'passsword')
cy.get("#pass-strength-result")
.should('have.text', 'Weak')
.should('have.class', 'bad')
cy.get("input[name='admin_password']")
.clear()
.type("Tr0ub4dour&3")
.should('have.value', 'Tr0ub4dour&3')
cy.get("#pass-strength-result")
.should('have.text', 'Good')
.should('have.class', 'good')
cy.get("input[name='admin_password']")
.clear()
.type("#Tr0ub4dour&3#")
.should('have.value', '#Tr0ub4dour&3#')
cy.get("#pass-strength-result")
.should('have.text', 'Strong')
.should('have.class', 'strong')
cy.get("input[name='admin_password']")
.clear()
.type("correcthorsebatterystaple")
.should('have.value', 'correcthorsebatterystaple')
cy.get("#pass-strength-result")
.should('have.text', 'Very strong')
.should('have.class', 'strong')
cy.get("input[name='admin_visible']").check()
cy.get("input[name='admin_email']")
.clear()
.type("admin_email@mail.local")
.should('have.value', 'admin_email@mail.local')
cy.get("input[name='owner']")
.clear()
.type("owner")
.should('have.value', 'owner')
cy.get(".button").click();
cy.contains("Great job, man!");
cy.exec("touch install/runInstall.php", {failOnNonZeroExit: false});
cy.get(".login").click();
cy.contains("Login");
})
});

View File

@ -1,20 +0,0 @@
describe("Login and logout", () => {
it('Login', function () {
cy.login()
cy.contains("Logs")
cy.contains("Logs").click()
cy.wait('@ajax_log')
cy.get("#list").contains("Login")
})
it('Logout', function () {
cy.login()
cy.visit("/logout.php")
cy.contains("Login")
cy.login()
cy.contains("Logs")
cy.contains("Logs").click()
cy.wait('@ajax_log')
cy.get("#list").contains("Logout")
})
});

View File

@ -1,28 +0,0 @@
describe("Availability", () => {
beforeEach(() => {
cy.login()
})
it('Change availability to available', function () {
cy.contains('Activate').click()
cy.wait("@ajax_change_availability")
cy.get(".toast-message").should('be.visible').contains('Thanks, admin, you have given your availability in case of alert.');
cy.wait("@ajax_list")
cy.get(".fa-check").should('be.visible')
cy.visit("/log.php")
cy.wait("@ajax_log")
cy.contains("Status changed to 'available'")
cy.visit("/")
})
it('Change availability to not available', function () {
cy.contains('Deactivate').click()
cy.wait("@ajax_change_availability")
cy.get(".toast-message").should('be.visible').contains('Thanks, admin, you have removed your availability in case of alert.');
cy.wait("@ajax_list")
cy.get(".fa-times").should('be.visible')
cy.visit("/log.php")
cy.wait("@ajax_log")
cy.contains("Status changed to 'not available'")
cy.visit("/")
})
});

View File

@ -1,92 +0,0 @@
describe("User management", () => {
before(() => {
cy.login()
cy.fixture('users')
.as('users');
})
it('Create users', () => {
cy.get('@users')
.then((users) => {
cy.getApiKey().then((apiKey) => {
var i = 1
var phone_number = "+1-800-555-0199";
users.forEach(user => {
console.log("User '"+user.name+"' number "+i);
if(i == 1){
console.log("Adding user via gui...");
name = user.name
console.log(user)
cy.wait(1000)
cy.contains("Add user").click()
cy.get("input[name='mail']")
.clear()
.type(user.email)
.should('have.value', user.email)
cy.get("input[name='name']")
.clear()
.type(user.name)
.should('have.value', user.name)
cy.get("input[name='username']")
.clear()
.type(user.username)
.should('have.value', user.username)
cy.get("input[name='password']")
.clear()
.type(user.password)
.should('have.value', user.password)
cy.get("input[name='phone_number']")
.clear()
.type(phone_number)
.should('have.value', phone_number)
cy.get("input[name='birthday']")
.clear()
.type(user.birthday)
.should('have.value', user.birthday)
if(user.chief){
cy.get("input[name='chief']")
.check({force: true})
}
if(user.driver){
cy.get("input[name='driver']")
.check({force: true})
}
cy.contains("Submit").click()
cy.wait('@ajax_list')
cy.contains(user.name)
cy.visit("/log.php")
cy.wait('@ajax_log')
cy.contains("User added")
cy.contains(user.name)
cy.visit("/")
} else {
console.log("Adding user via api...");
cy.request({ method: 'POST', url: '/api.php/user', form: true,
body: {
apiKey: apiKey,
mail: user.email,
name: user.name,
username: user.username,
password: user.password,
phone_number: phone_number,
birthday: user.birthday,
chief: user.chief | 0,
driver: user.driver | 0,
hidden: 0,
disabled: 0
}})
.then((response) => {
console.log(response.body)
expect(response.status).to.eq(200)
expect(response.body).to.have.property('userId')
cy.visit("/log.php")
cy.wait('@ajax_log')
cy.contains("User added")
cy.contains(user.name)
})
}
i+=1;
})
});
})
});
})

View File

@ -1,110 +0,0 @@
describe("Service management", () => {
beforeEach(() => {
cy.login();
})
it('Add Service with new type', function () {
cy.get('tr:has(> td:has(> [data-user="11"])) > :nth-child(6)').should('contain', '0');
cy.get('tr:has(> td:has(> [data-user="4"])) > :nth-child(6)').should('contain', '0');
cy.get('tr:has(> td:has(> [data-user="9"])) > :nth-child(6)').should('contain', '0');
cy.get('tr:has(> td:has(> [data-user="7"])) > :nth-child(6)').should('contain', '0');
cy.get('tr:has(> td:has(> [data-user="2"])) > :nth-child(6)').should('contain', '0');
cy.get('tr:has(> td:has(> [data-user="6"])) > :nth-child(6)').should('contain', '0');
cy.visit("/edit_service.php?add", {
onBeforeLoad(win) {
cy.stub(win, 'prompt').returns('test');
}
});
cy.get('#date-picker').clear();
cy.get('#date-picker').type('07/12/2020');
cy.window().then(win => win.$('.datepicker').remove());
cy.get('#progressivo').clear();
cy.get('#progressivo').type('1234/5');
cy.get('#timePicker1').clear();
cy.get('#timePicker1').type('10:10');
cy.get('#timePicker2').clear();
cy.get('#timePicker2').type('23:59');
cy.get('.chief-11').check();
cy.get('.drivers-7').check();
cy.get('.drivers-6').check();
cy.get('.crew-2').check();
cy.get('.crew-4').check();
cy.get('.crew-9').check();
cy.get('#addr').clear();
cy.get('#addr').type('brescia');
cy.get('#search_button').click();
cy.get('#search').click();
cy.get('.results-list > :nth-child(1) > a').click();
cy.get('#notes').click();
cy.get('#types').select('add_new');
cy.wait('@ajax_add_type');
cy.get('[type="submit"]').click();
cy.wait('@ajax_services');
cy.contains("2020-07-12");
cy.contains("1234/5");
cy.visit("/log.php");
cy.wait('@ajax_log');
cy.contains("Service added");
cy.visit("/list.php");
cy.wait('@ajax_list');
cy.get('tr:has(> td:has(> [data-user="11"])) > :nth-child(6)').should('contain', '1');
cy.get('tr:has(> td:has(> [data-user="4"])) > :nth-child(6)').should('contain', '1');
cy.get('tr:has(> td:has(> [data-user="9"])) > :nth-child(6)').should('contain', '1');
cy.get('tr:has(> td:has(> [data-user="7"])) > :nth-child(6)').should('contain', '1');
cy.get('tr:has(> td:has(> [data-user="2"])) > :nth-child(6)').should('contain', '1');
cy.get('tr:has(> td:has(> [data-user="6"])) > :nth-child(6)').should('contain', '1');
});
it('Edit service', function() {
cy.visit("/services.php");
cy.wait('@ajax_services');
cy.get('.dtr-details a[data-action="edit"]').first().click();
cy.get('#progressivo').clear();
cy.get('#progressivo').type('4321/5');
cy.get('.chief-11').uncheck();
cy.get('.chief-8').check();
cy.get('.crew-4').uncheck();
cy.get('.crew-9').uncheck();
cy.get('.crew-3').check();
cy.get('.crew-5').check();
cy.get('#addr').clear();
cy.get('#addr').type('milano');
cy.get('#search_button').click();
cy.get('.results-list > :nth-child(1) > a').click();
cy.get('[type="submit"]').click();
cy.wait('@ajax_services');
cy.contains("2020-07-12");
cy.contains("4321/5");
cy.visit("/log.php");
cy.wait('@ajax_log');
cy.contains("Service edited");
cy.visit("/list.php");
cy.wait('@ajax_list');
cy.get('tr:has(> td:has(> [data-user="11"])) > :nth-child(6)').should('contain', '0');
cy.get('tr:has(> td:has(> [data-user="8"])) > :nth-child(6)').should('contain', '1');
cy.get('tr:has(> td:has(> [data-user="4"])) > :nth-child(6)').should('contain', '0');
cy.get('tr:has(> td:has(> [data-user="9"])) > :nth-child(6)').should('contain', '0');
cy.get('tr:has(> td:has(> [data-user="3"])) > :nth-child(6)').should('contain', '1');
cy.get('tr:has(> td:has(> [data-user="5"])) > :nth-child(6)').should('contain', '1');
});
it('Delete Service', function() {
cy.visit("/services.php");
cy.wait('@ajax_services');
cy.get('.dtr-details a[data-action="delete"]').first().click();
cy.get('#remove').click();
cy.wait('@ajax_services');
cy.visit("/log.php");
cy.wait('@ajax_log');
cy.contains("Service removed");
cy.visit("/list.php");
cy.wait('@ajax_list');
cy.get('tr:has(> td:has(> [data-user="8"])) > :nth-child(6)').should('contain', '0');
cy.get('tr:has(> td:has(> [data-user="3"])) > :nth-child(6)').should('contain', '0');
cy.get('tr:has(> td:has(> [data-user="5"])) > :nth-child(6)').should('contain', '0');
cy.get('tr:has(> td:has(> [data-user="7"])) > :nth-child(6)').should('contain', '0');
cy.get('tr:has(> td:has(> [data-user="2"])) > :nth-child(6)').should('contain', '0');
cy.get('tr:has(> td:has(> [data-user="6"])) > :nth-child(6)').should('contain', '0');
});
})

View File

@ -1,78 +0,0 @@
describe("Training management", () => {
beforeEach(() => {
cy.login();
})
it('Add Training', function () {
cy.visit("/edit_training.php?add", {
onBeforeLoad(win) {
cy.stub(win, 'prompt').returns('test')
}
});
cy.get('#date-picker').clear();
cy.get('#date-picker').type('07/12/2020');
cy.window().then(win => win.$('.datepicker').remove());
cy.get('#name').clear();
cy.get('#name').type('Test Training');
cy.get('#timePicker1').clear();
cy.get('#timePicker1').type('10:10');
cy.get('#timePicker2').clear();
cy.get('#timePicker2').type('23:59');
cy.get('.chief-5').check();
cy.get('.crew-2').check();
cy.get('.crew-4').check();
cy.get('.crew-3').check();
cy.get('.crew-6').check();
cy.get('#addr').clear();
cy.get('#addr').type('brescia');
cy.get('#search_button').click();
cy.get('#search').click();
cy.get('.results-list > :nth-child(1) > a').click();
cy.get('[type="submit"]').click();
cy.wait('@ajax_trainings');
cy.contains("2020-07-12");
cy.contains("Test Training");
cy.visit("/log.php");
cy.wait('@ajax_log');
cy.contains("Training added");
cy.visit("/list.php");
cy.wait('@ajax_list');
});
it('Edit Training', function() {
cy.visit("/trainings.php");
cy.wait('@ajax_trainings');
cy.get('.dtr-details a[data-action="edit"]').first().click();
cy.get('#name').clear();
cy.get('#name').type('Training 1 test');
cy.get('.chief-5').uncheck();
cy.get('.chief-7').check();
cy.get('.crew-3').uncheck();
cy.get('.crew-6').uncheck();
cy.get('.crew-9').check();
cy.get('.crew-8').check();
cy.get('#addr').clear();
cy.get('#addr').type('milano');
cy.get('#search_button').click();
cy.get('.results-list > :nth-child(1) > a').click();
cy.get('[type="submit"]').click();
cy.wait('@ajax_trainings');
cy.contains("2020-07-12");
cy.contains("Training 1 test");
cy.visit("/log.php");
cy.wait('@ajax_log');
cy.contains("Training edited");
});
it('Delete Training', function() {
cy.visit("/trainings.php");
cy.wait('@ajax_trainings');
cy.get('.dtr-details a[data-action="delete"]').first().click();
cy.get('#remove').click();
cy.wait('@ajax_trainings');
cy.visit("/log.php");
cy.wait('@ajax_log');
cy.contains("Training removed");
});
})

View File

@ -1,47 +0,0 @@
Cypress.on('uncaught:exception', (err, runnable) => {
// for some reasons, the test fails without this in certain conditions...
return false
})
//TODO: login remember me and better language support
Cypress.Commands.add("login", (username="admin", password="correcthorsebatterystaple") => {
cy.setCookie("forceLanguage", "en");
cy.setCookie('disableServiceWorkerInstallation', '1');
cy.visit("/");
cy.get("input[name='name']")
.clear()
.type(username)
.should('have.value', username)
cy.get("input[name='password']")
.clear()
.type(password)
.should('have.value', password)
cy.get("input[name='login']").click()
})
Cypress.Commands.add("getApiKey", (username="admin", password="correcthorsebatterystaple") => {
cy.request({ method: 'POST', url: '/api.php/login', form: true, body: { username: username, password: password }})
.then((response) => {
expect(response.status).to.eq(200)
expect(response.body).to.have.property('apiKey')
console.log(response.body)
return response.body.apiKey
})
})
beforeEach(() => {
cy.intercept('https://nominatim.openstreetmap.org/search?format=json&limit=5&q=brescia', { fixture: 'nominatim_brescia.json' });
cy.intercept('https://nominatim.openstreetmap.org/search?format=json&limit=5&q=milano', { fixture: 'nominatim_milano.json' });
cy.intercept('https://a.tile.openstreetmap.org/*/*/*.png', { fixture: 'map_frame_A.png' });
cy.intercept('https://b.tile.openstreetmap.org/*/*/*.png', { fixture: 'map_frame_B.png' });
cy.intercept('https://c.tile.openstreetmap.org/*/*/*.png', { fixture: 'map_frame_C.png' });
cy.intercept(Cypress.config('baseUrl')+'resources/ajax/ajax_add_type.php**').as('ajax_add_type');
cy.intercept(Cypress.config('baseUrl')+'resources/ajax/ajax_change_availability.php**').as('ajax_change_availability');
cy.intercept(Cypress.config('baseUrl')+'resources/ajax/ajax_list.php**').as('ajax_list');
cy.intercept(Cypress.config('baseUrl')+'resources/ajax/ajax_log.php**').as('ajax_log');
cy.intercept(Cypress.config('baseUrl')+'resources/ajax/ajax_services.php**').as('ajax_services');
cy.intercept(Cypress.config('baseUrl')+'resources/ajax/ajax_trainings.php**').as('ajax_trainings');
});

View File

@ -1,14 +0,0 @@
<?php
require("core.php");
init_class();
if($user->authenticated()){
if($user->hasRole(Role::DEVELOPER)){
if(!isset($_REQUEST["op"]) || !isset($_REQUEST["id"])) $tools->rickroll();
$openHandler = new DebugBar\OpenHandler($debugbar);
$response = $openHandler->handle();
} else {
$tools->rickroll();
}
} else {
$tools->rickroll();
}

View File

@ -1 +0,0 @@
nothing (see this directory in file explorer or delete this file)

View File

@ -1,77 +0,0 @@
<?php
require_once 'ui.php';
function debug(){
echo("<pre>"); var_dump($_POST); echo("</pre>"); exit();
}
if($tools->validate_form("mod", "add")) {
if($tools->validate_form(['date', 'code', 'beginning', 'end', 'place', 'notes', 'type', 'token'])) {
if($_POST["token"] == $_SESSION['token']) {
bdump("adding service");
$place = $tools->checkPlaceParam($_POST["place"]);
$crud->add_service($_POST["date"], $_POST["code"], $_POST["beginning"], $_POST["end"], $_POST["chief"][0], $tools->extract_unique($_POST["drivers"]), $tools->extract_unique($_POST["crew"]), $place, $_POST["notes"], $_POST["type"], $tools->extract_unique([$_POST["chief"],$_POST["drivers"],$_POST["crew"]]), $user->name());
$tools->redirect("services.php");
} else {
debug(); //TODO: remove debug info
}
} else {
debug();
}
} elseif($tools->validate_form("mod", "edit")) {
if($tools->validate_form(['id', 'date', 'code', 'beginning', 'end', 'place', 'notes', 'type', 'token'])) {
if($_POST["token"] == $_SESSION['token']) {
bdump($_POST);
bdump("editing service");
$place = $tools->checkPlaceParam($_POST["place"]);
$crud->edit_service($_POST["id"], $_POST["date"], $_POST["code"], $_POST["beginning"], $_POST["end"], $_POST["chief"][0], $tools->extract_unique($_POST["drivers"]), $tools->extract_unique($_POST["crew"]), $place, $_POST["notes"], $_POST["type"], $tools->extract_unique([$_POST["chief"],$_POST["drivers"],$_POST["crew"]]), $user->name());
$tools->redirect("services.php");
} else {
debug();
}
} else {
debug();
}
} elseif($tools->validate_form("mod", "delete")) {
bdump("removing service");
if($tools->validate_form(['id', 'token'])) {
if($_POST["token"] == $_SESSION['token']) {
bdump("removing service");
$crud->remove_service($_POST["id"]);
$tools->redirect("services.php");
} else {
echo("1");
debug();
}
} else {
echo("2");
debug();
}
} else {
if(isset($_GET["add"])||isset($_GET["edit"])||isset($_GET["delete"])||isset($_GET["mod"])) {
$_SESSION["token"] = bin2hex(random_bytes(64));
}
$crew = $db->select("SELECT * FROM `".DB_PREFIX."_profiles` ORDER BY name ASC");
$types = $db->select("SELECT `name` FROM `".DB_PREFIX."_type` ORDER BY name ASC");
$modalità = (isset($_GET["add"])) ? "add" : ((isset($_GET["edit"])) ? "edit" : ((isset($_GET["delete"])) ? "delete" : "add"));
bdump($modalità, "modalità");
bdump($types, "types");
bdump($crew, "crew");
$id = "";
if(isset($_GET["id"])) {
$id = $_GET["id"];
bdump($crud->exists("services", $id));
$values = $db->select("SELECT * FROM `".DB_PREFIX."_services` WHERE `id` = :id", [":id" => $id])[0];
bdump($values);
} else {
$values = [];
}
if($modalità=="edit" || $modalità=="delete") {
if(empty($id)) {
echo("<pre>"); var_dump($_POST); echo("</pre>");
} elseif (!$crud->exists("services", $id)) {
echo("<pre>"); var_dump($_POST); echo("</pre>");
}
}
loadtemplate('edit_service.html', ['service' => ['id' => $id, 'token' => $_SESSION['token'], 'modalità' => $modalità, 'crew' => $crew, 'types' => $types], 'values' => $values, 'title' => t(ucfirst($modalità) . " service", false)]);
bdump($_SESSION['token'], "token");
}
?>

View File

@ -1,73 +0,0 @@
<?php
require_once 'ui.php';
function debug(){
echo("<pre>"); var_dump($_POST); echo("</pre>"); exit();
}
if($tools->validate_form("mod", "add")) {
if($tools->validate_form(['date', 'name', 'start_time', 'end_time', 'place', 'notes', 'token'])) {
if($_POST["token"] == $_SESSION['token']) {
bdump("adding training");
$place = $tools->checkPlaceParam($_POST["place"]);
$crud->add_training($_POST["date"], $_POST["name"], $_POST["start_time"], $_POST["end_time"], $_POST["chief"][0], $tools->extract_unique($_POST["crew"]), $place, $_POST["notes"], $tools->extract_unique([$_POST["chief"],$_POST["crew"]]), $user->name());
$tools->redirect("trainings.php");
} else {
debug(); //TODO: remove debug info
}
} else {
debug();
}
} elseif($tools->validate_form("mod", "edit")) {
if($tools->validate_form(['id', 'date', 'name', 'start_time', 'end_time', 'chief', 'place', 'notes', 'token'])) {
if($_POST["token"] == $_SESSION['token']) {
bdump($_POST);
bdump("editing training");
$place = $tools->checkPlaceParam($_POST["place"]);
$crud->edit_training($_POST["id"], $_POST["date"], $_POST["name"], $_POST["start_time"], $_POST["end_time"], $_POST["chief"][0], $tools->extract_unique($_POST["crew"]), $place, $_POST["notes"], $tools->extract_unique([$_POST["chief"],$_POST["crew"]]), $user->name());
$tools->redirect("trainings.php");
} else {
debug();
}
} else {
debug();
}
} elseif($tools->validate_form("mod", "delete")) {
bdump("removing training");
if($tools->validate_form(['id', 'token'])) {
if($_POST["token"] == $_SESSION['token']) {
bdump("removing training");
$crud->remove_training($_POST["id"]);
$tools->redirect("trainings.php");
} else {
debug();
}
} else {
debug();
}
} else {
if(isset($_GET["add"])||isset($_GET["edit"])||isset($_GET["delete"])||isset($_GET["mod"])) {
$_SESSION["token"] = bin2hex(random_bytes(64));
}
$crew = $db->select("SELECT * FROM `".DB_PREFIX."_profiles` ORDER BY name ASC");
$modalità = (isset($_GET["add"])) ? "add" : ((isset($_GET["edit"])) ? "edit" : ((isset($_GET["delete"])) ? "delete" : "add"));
bdump($modalità, "modalità");
bdump($crew, "crew");
$id = "";
if(isset($_GET["id"])) {
$id = $_GET["id"];
bdump($crud->exists("trainings", $id));
$values = $db->select("SELECT * FROM `".DB_PREFIX."_trainings` WHERE `id` = :id", [":id" => $id])[0];
bdump($values);
} else {
$values = [];
}
if($modalità=="edit" || $modalità=="delete") {
if(empty($id)) {
$tools->redirect("accessdenied.php");
} elseif (!$crud->exists("trainings", $id)) {
//$tools->redirect("accessdenied.php");
}
}
loadtemplate('edit_training.html', ['training' => ['id' => $id, 'token' => $_SESSION['token'], 'modalità' => $modalità, 'crew' => $crew], 'values' => $values, 'title' => t(ucfirst($modalità) . " training", false)]);
bdump($_SESSION['token'], "token");
}
?>

View File

@ -1,95 +0,0 @@
<?php
require_once 'ui.php';
use Brick\PhoneNumber\PhoneNumber;
use Brick\PhoneNumber\PhoneNumberFormat;
use Brick\PhoneNumber\PhoneNumberParseException;
if(!$user->hasRole(Role::SUPER_ADMIN)){
require("error_page.php");
show_error_page(401, t("You are not authorized to perform this action.",false), "");
exit();
}
function debug(){
echo("<pre>"); var_dump($_POST); echo("</pre>"); exit();
}
if($tools->validate_form("mod", "add")) {
if($tools->validate_form(['mail', 'name', 'username', 'password', 'phone_number', 'birthday', 'token'])) {
if($_POST["token"] == $_SESSION['token']) {
bdump("adding user");
bdump($_POST);
$chief = isset($_POST["chief"]) ? 1 : 0;
$driver = isset($_POST["driver"]) ? 1 : 0;
$hidden = isset($_POST["visible"]) ? 0 : 1;
$disabled = isset($_POST["enabled"]) ? 0 : 1;
try {
$phone_number = PhoneNumber::parse($_POST["phone_number"]);
if (!$phone_number->isValidNumber()) {
echo("Bad phone number. <a href='javascript:window.history.back()'>Go back</a>"); //TODO: better form validation
exit();
} else {
$phone_number = $phone_number->format(PhoneNumberFormat::E164);
}
} catch (PhoneNumberParseException $e) {
echo("Bad phone number. <a href='javascript:window.history.back()'>Go back</a>"); //TODO: better form validation
exit();
}
$user->add_user($_POST["mail"], $_POST["name"], $_POST["username"], $_POST["password"], $phone_number, $_POST["birthday"], $chief, $driver, $hidden, $disabled, $user->name());
$tools->redirect("list.php");
} else {
debug();
}
} else {
debug();
}
/*} elseif($tools->validate_form("mod", "edit")) {
if($tools->validate_form(['mail', 'name', 'username', 'password', 'birthday', 'token'])) {
if($_POST["token"] == $_SESSION['token']){
bdump($_POST);
bdump("editing service");
$crud->edit_service($_POST["id"], $_POST["date"], $_POST["code"], $_POST["beginning"], $_POST["end"], $_POST["chief"], $tools->extract_unique($_POST["drivers"]), $tools->extract_unique($_POST["crew"]), $_POST["place"], $_POST["notes"], $_POST["type"], $tools->extract_unique([$_POST["chief"],$_POST["drivers"],$_POST["crew"]]), $user->name());
$tools->redirect("services.php");
} else {
$tools->redirect("accessdenied.php");
}
}
*/
} elseif($tools->validate_form("mod", "delete")) {
bdump("removing service");
if($tools->validate_form(['id', 'token'])) {
if($_POST["token"] == $_SESSION['token']) {
bdump("removing user");
$user->remove_user($_POST["id"]);
$tools->redirect("list.php");
} else {
debug();
}
} else {
debug();
}
} else {
if(isset($_GET["add"])||isset($_GET["edit"])||isset($_GET["delete"])||isset($_GET["mod"])) {
$_SESSION["token"] = bin2hex(random_bytes(64));
}
$modalità = (isset($_GET["add"])) ? "add" : ((isset($_GET["edit"])) ? "edit" : ((isset($_GET["delete"])) ? "delete" : "add"));
bdump($modalità, "modalità");
$id = "";
if(isset($_GET["id"])) {
$id = $_GET["id"];
bdump($crud->exists("profiles", $id));
$values = $db->select("SELECT * FROM `".DB_PREFIX."_profiles` WHERE `id` = :id", [":id" => $id])[0];
bdump($values);
} else {
$values = [];
}
if($modalità=="edit" || $modalità=="delete") {
if(empty($id)) {
$tools->redirect("accessdenied.php");
} elseif (!$crud->exists("profiles", $id)) {
$tools->redirect("accessdenied.php");
}
}
loadtemplate('edit_user.html', ['id' => $id, 'token' => $_SESSION["token"], 'modalità' => $modalità, 'values' => $values, 'title' => t(ucfirst($modalità) . " user", false)]);
bdump($_SESSION['token'], "token");
}
?>

View File

@ -1,98 +0,0 @@
<?php
function show_error_page($error=null, $error_message=null, $error_message_advanced=null){
global $tools, $webpack_manifest_path;
$error = !is_null($error) ? $error : (isset($_GET["error"]) ? $_GET["error"] : 404);
if(is_null($error_message)){
switch ($error){
case 404:
$error_message = "Page not found";
$error_message_advanced = "Please check the url";
break;
case 500:
$error_message = "Server error";
$error_message_advanced = "Please retry later";
break;
}
}
$main_script_url = null;
$game_script_url = null;
try{
$webpack_manifest_path = isset($webpack_manifest_path) ? $webpack_manifest_path : realpath("resources/dist/assets-manifest.json");
if(!empty($webpack_manifest_path)){
$webpack_manifest = json_decode(
file_get_contents($webpack_manifest_path),
true
);
$main_script_url = "resources/dist/".$webpack_manifest["main.js"]["src"];
$game_script_url = "resources/dist/".$webpack_manifest["games.js"]["src"];
}
} catch(\Exception $e) {
}
$error_templates = [
<<<EOT
<div id="error">
<div id="box"></div>
<h3>ERROR $error</h3>
<p>$error_message</p>
<p>$error_message_advanced</p>
</div>
<style>
body,html{height:100%}body{display:grid;width:100%;font-family:Inconsolata,monospace}body div#error{position:relative;margin:auto;padding:20px;z-index:2}body div#error div#box{position:absolute;top:0;left:0;width:100%;height:100%;border:1px solid #000}body div#error div#box:after,body div#error div#box:before{content:'';position:absolute;top:0;left:0;width:100%;height:100%;box-shadow:inset 0 0 0 1px #000;mix-blend-mode:multiply;animation:dance 2s infinite steps(1)}body div#error div#box:before{clip-path:polygon(0 0,65% 0,35% 100%,0 100%);box-shadow:inset 0 0 0 1px currentColor;color:#f0f}body div#error div#box:after{clip-path:polygon(65% 0,100% 0,100% 100%,35% 100%);animation-duration:.5s;animation-direction:alternate;box-shadow:inset 0 0 0 1px currentColor;color:#0ff}body div#error h3{position:relative;font-size:5vw;font-weight:700;text-transform:uppercase;animation:blink 1.3s infinite steps(1)}body div#error h3:after,body div#error h3:before{content:'ERROR $error';position:absolute;top:-1px;left:0;mix-blend-mode:soft-light;animation:dance 2s infinite steps(2)}body div#error h3:before{clip-path:polygon(0 0,100% 0,100% 50%,0 50%);color:#f0f;animation:shiftright 2s steps(2) infinite}body div#error h3:after{clip-path:polygon(0 100%,100% 100%,100% 50%,0 50%);color:#0ff;animation:shiftleft 2s steps(2) infinite}body div#error p{position:relative;margin-bottom:8px}body div#error p span{position:relative;display:inline-block;font-weight:700;color:#000;animation:blink 3s steps(1) infinite}body div#error p span:after,body div#error p span:before{content:'unstable';position:absolute;top:-1px;left:0;mix-blend-mode:multiply}body div#error p span:before{clip-path:polygon(0 0,100% 0,100% 50%,0 50%);color:#f0f;animation:shiftright 1.5s steps(2) infinite}body div#error p span:after{clip-path:polygon(0 100%,100% 100%,100% 50%,0 50%);color:#0ff;animation:shiftleft 1.7s steps(2) infinite}@-moz-keyframes dance{0%,84%,94%{transform:skew(0)}85%{transform:skew(5deg)}90%{transform:skew(-5deg)}98%{transform:skew(3deg)}}@-webkit-keyframes dance{0%,84%,94%{transform:skew(0)}85%{transform:skew(5deg)}90%{transform:skew(-5deg)}98%{transform:skew(3deg)}}@-o-keyframes dance{0%,84%,94%{transform:skew(0)}85%{transform:skew(5deg)}90%{transform:skew(-5deg)}98%{transform:skew(3deg)}}@keyframes dance{0%,84%,94%{transform:skew(0)}85%{transform:skew(5deg)}90%{transform:skew(-5deg)}98%{transform:skew(3deg)}}@-moz-keyframes shiftleft{0%,100%,87%{transform:translate(0,0) skew(0)}84%,90%{transform:translate(-8px,0) skew(20deg)}}@-webkit-keyframes shiftleft{0%,100%,87%{transform:translate(0,0) skew(0)}84%,90%{transform:translate(-8px,0) skew(20deg)}}@-o-keyframes shiftleft{0%,100%,87%{transform:translate(0,0) skew(0)}84%,90%{transform:translate(-8px,0) skew(20deg)}}@keyframes shiftleft{0%,100%,87%{transform:translate(0,0) skew(0)}84%,90%{transform:translate(-8px,0) skew(20deg)}}@-moz-keyframes shiftright{0%,100%,87%{transform:translate(0,0) skew(0)}84%,90%{transform:translate(8px,0) skew(20deg)}}@-webkit-keyframes shiftright{0%,100%,87%{transform:translate(0,0) skew(0)}84%,90%{transform:translate(8px,0) skew(20deg)}}@-o-keyframes shiftright{0%,100%,87%{transform:translate(0,0) skew(0)}84%,90%{transform:translate(8px,0) skew(20deg)}}@keyframes shiftright{0%,100%,87%{transform:translate(0,0) skew(0)}84%,90%{transform:translate(8px,0) skew(20deg)}}@-moz-keyframes blink{0%,100%,50%,85%{color:#000}87%,95%{color:transparent}}@-webkit-keyframes blink{0%,100%,50%,85%{color:#000}87%,95%{color:transparent}}@-o-keyframes blink{0%,100%,50%,85%{color:#000}87%,95%{color:transparent}}@keyframes blink{0%,100%,50%,85%{color:#000}87%,95%{color:transparent}}
</style>
EOT,
<<<EOT
<h1 aria-label="$error Error" style="font-size: calc(3em + 9vw);">$error</h1>
<h2>$error_message</h2>
<h3>$error_message_advanced</h3>
<style>
body {
justify-content: center;
align-items: center;
font-family: 'Open Sans', Arial, sans-serif;
text-align: center;
color: #fff;
background-image: linear-gradient(-225deg, #cf2778, #7c64d5, #4cc3ff);
}
</style>
EOT
];
$credits_list = [
"<a target='_blank' href='https://codepen.io/yexx'>Yeshua Emanuel Braz</a>",
"<a target='_blank' href='https://codepen.io/GeorgePark'>George W. Park</a>"
];
$key = isset($_GET["force_page"]) ? $_GET["force_page"] : array_rand($error_templates);
$credits = $credits_list[$key];
echo($error_templates[$key]);
$nonce = false;
try {
if (is_object($tools)) $nonce = $tools->script_nonce;
} catch (\Exception $e) {
}
?>
<br><br>
<?php
if(!is_null($game_script_url)){
?>
<div class="games_list" style="margin-left: 20px; text-align: left;">
While you are waiting, you can play some games:
<ul>
<li hidden><a data-game="pong" class="playGame">Pong</a></li>
<li><a data-game="ld46" class="playGame">What the firetruck</a></li>
</ul>
</div>
<div class="credits" style="position:absolute;opacity: 0.5;bottom: 5px;right: 5px;">
Error page based on work by <?php echo($credits); ?>.
</div>
<script src="<?php echo($main_script_url); ?>"<?php if($nonce){ echo("nonce='{$nonce}'"); } ?>></script>
<script src="<?php echo($game_script_url); ?>"<?php if($nonce){ echo("nonce='{$nonce}'"); } ?>></script>
<?php
}
}
if (basename(__FILE__) == basename($_SERVER["SCRIPT_FILENAME"])) {
show_error_page();
}
?>

View File

@ -1,199 +0,0 @@
<?php
declare(strict_types=1);
// Test this using following command
// php -S localhost:8080 ./graphql.php &
// curl http://localhost:8080 -d '{"query": "query { echo(message: \"Hello World\") }" }'
// curl http://localhost:8080 -d '{"query": "mutation { sum(x: 2, y: 2) }" }'
require_once __DIR__ . '/vendor/autoload.php';
use GraphQL\GraphQL;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;
$users = [[
'email' => "email",
'username' => "username",
'name' => "Name",
'available' => true,
'chief' => true,
'driver' => true,
'phoneNumber' => "+11234567891",
'services' => 0,
'trainings' => 0,
'availabilityMinutes' => 0,
'verified' => true,
'hidden' => false,
'disabled' => false,
],[
'email' => "email2",
'username' => "username2",
'name' => "Name2",
'available' => true,
'chief' => true,
'driver' => true,
'phoneNumber' => "+11234567892",
'services' => 0,
'trainings' => 0,
'availabilityMinutes' => 0,
'verified' => true,
'hidden' => false,
'disabled' => false,
]];
try {
$userType = new ObjectType([
'name' => 'User',
'description' => 'Allerta User',
'fields' => [
'id' => Type::int(),
'email' => Type::string(),
'username' => Type::string(),
'name' => Type::string(),
'available' => Type::boolean(),
'chief' => Type::boolean(),
'driver' => Type::boolean(),
'phoneNumber' => Type::string(), //TODO: custom type
'services' => Type::int(),
'trainings' => Type::int(),
'availabilityMinutes' => Type::int(),
'verified' => Type::boolean(),
'hidden' => Type::boolean(),
'disabled' => Type::boolean(),
]
]);
$queryType = new ObjectType([
'name' => 'Query',
'fields' => [
'echo' => [
'type' => Type::string(),
'args' => [
'message' => ['type' => Type::string()],
],
'resolve' => static function ($rootValue, array $args): string {
return $rootValue['prefix'] . $args['message'];
},
],
'user' => [
'type' => $userType,
'args' => [
'id' => [
'type' => Type::int(),
]
],
'resolve' => function ($rootValue, array $args) {
global $users;
return $users[0];
},
],
'Users' => [
'type' => Type::listOf($userType),
'args' => [
'id' => [
'type' => Type::int(),
],
'username' => [
'type' => Type::string(),
],
'available' => [
'type' => Type::boolean(),
],
'chief' => [
'type' => Type::boolean(),
],
'driver' => [
'type' => Type::boolean(),
],
'services' => [
'type' => Type::int(),
],
'trainings' => [
'type' => Type::int(),
],
'availabilityMinutes' => [
'type' => Type::int(),
],
'verified' => [
'type' => Type::boolean(),
],
'hidden' => [
'type' => Type::boolean(),
],
'disabled' => [
'type' => Type::boolean()
]
],
'resolve' => function ($rootValue, array $args) {
global $db, $user;
$profiles = $db->select("SELECT * FROM `".DB_PREFIX."_profiles`");
$users = $db->select("SELECT * FROM `".DB_PREFIX."_users`");
$result = [];
for ($i=0; $i < sizeof($profiles); $i++) {
$result[] = [
"id" => $users["id"],
"email" => $profiles["email"],
"username" => $users["username"],
"name" => $user->nameById($users["id"]),
"available" => $profiles["available"],
"chief" => $profiles["chief"],
"driver" => $profiles["driver"],
"phoneNumber" => $profiles["phone_number"],
"services" => $profiles["services"],
"trainings" => $profiles["trainings"],
"availabilityMinutes" => $profiles["availabilityMinutes"],
"verified" => $users["verified"],
"hidden" => $profiles["hidden"],
"disabled" => $profiles["disabled"],
];
}
return $result;
},
]
],
]);
$mutationType = new ObjectType([
'name' => 'Calc',
'fields' => [
'sum' => [
'type' => Type::int(),
'args' => [
'x' => ['type' => Type::int()],
'y' => ['type' => Type::int()],
],
'resolve' => static function ($calc, array $args): int {
return $args['x'] + $args['y'];
},
],
],
]);
// See docs on schema options:
// https://webonyx.github.io/graphql-php/type-system/schema/#configuration-options
$schema = new Schema([
'query' => $queryType,
'mutation' => $mutationType,
]);
$rawInput = file_get_contents('php://input');
$input = json_decode($rawInput, true);
$query = $input['query'];
$variableValues = $input['variables'] ?? null;
$rootValue = ['prefix' => 'You said: '];
$result = GraphQL::executeQuery($schema, $query, $rootValue, null, $variableValues);
$output = $result->toArray();
} catch (Throwable $e) {
$output = [
'error' => [
'message' => $e->getMessage(),
"object" => $e
],
];
}
header('Content-Type: application/json; charset=UTF-8');
echo json_encode($output);

View File

@ -1,16 +0,0 @@
<?php
require_once 'ui.php';
if($user->authenticated) {
$tools->redirect("list.php");
}
$error = false;
if(isset($_POST['name']) & isset($_POST['password'])) {
$login = $user->login($_POST['name'], $_POST['password'], isset($_POST["remember_me"]));
if($login===true) {
$tools->redirect("list.php");
} else {
$error = $login;
bdump($error);
}
}
loadtemplate('index.html', ['error' => $error, 'title' => t('Login', false)], false);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More