From 77326c49ecabd0cca1c52fe7480cd7ec0b8b7f02 Mon Sep 17 00:00:00 2001 From: Ente Date: Sat, 15 Feb 2025 20:33:36 +0100 Subject: [PATCH 01/48] TT-153: Reflect changes from v7.9 release into Mails.md --- api/v1/class/mails/Mails.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/api/v1/class/mails/Mails.md b/api/v1/class/mails/Mails.md index f869567..b2d1f45 100644 --- a/api/v1/class/mails/Mails.md +++ b/api/v1/class/mails/Mails.md @@ -35,20 +35,17 @@ You can implement your own MailTemplate like this: "Subject of the mail", - "body" => "Body of the mail", - "username" => "Username" - ] + return new MailTemplateData($data["subject"], $data["body"], $data["username"]); } } From fc901f514b45d57336b3612f0838550edac8b388 Mon Sep 17 00:00:00 2001 From: Ente Date: Thu, 20 Feb 2025 13:36:48 +0100 Subject: [PATCH 02/48] TT-163: Fix being unable to access "Forgot password" page --- suite/forgot_password.php | 1 - 1 file changed, 1 deletion(-) diff --git a/suite/forgot_password.php b/suite/forgot_password.php index 7c2a86d..3353b68 100644 --- a/suite/forgot_password.php +++ b/suite/forgot_password.php @@ -1,6 +1,5 @@ get_app_ini(); From 54da9e928d3612dbe66cfd4692e4ba88e832ffe7 Mon Sep 17 00:00:00 2001 From: Ente Date: Thu, 20 Feb 2025 13:39:05 +0100 Subject: [PATCH 03/48] TT-168: Remove usercount plugin entirely --- .../plugins/plugins/usercount/plugin.yml | 13 --- .../plugins/plugins/usercount/src/Main.php | 100 ------------------ .../plugins/plugins/usercount/views/test.php | 12 --- 3 files changed, 125 deletions(-) delete mode 100644 api/v1/class/plugins/plugins/usercount/plugin.yml delete mode 100644 api/v1/class/plugins/plugins/usercount/src/Main.php delete mode 100644 api/v1/class/plugins/plugins/usercount/views/test.php diff --git a/api/v1/class/plugins/plugins/usercount/plugin.yml b/api/v1/class/plugins/plugins/usercount/plugin.yml deleted file mode 100644 index dade5d2..0000000 --- a/api/v1/class/plugins/plugins/usercount/plugin.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: usercount -src: /src -main: Main -namespace: userdetail -author: none -description: Add User details -version: "1.0" -api: 0.1 -permissions: none -enabled: false -custom.values: -nav_links: - Get User Count: views/test.php \ No newline at end of file diff --git a/api/v1/class/plugins/plugins/usercount/src/Main.php b/api/v1/class/plugins/plugins/usercount/src/Main.php deleted file mode 100644 index 7bc0e7b..0000000 --- a/api/v1/class/plugins/plugins/usercount/src/Main.php +++ /dev/null @@ -1,100 +0,0 @@ -plugin_configuration["version"]; - $this->log_append = "[ExamplePlugin v{$v}]"; - } - - public function get_log_append(): string{ - return $this->log_append; - } - - public function set_plugin_configuration(): void{ - $this->plugin_configuration = $this->read_plugin_configuration("usercount"); - } - - public function get_plugin_configuration(): array{ - return $this->plugin_configuration; - } - - public function __construct(){ - $this->set_plugin_configuration(); - $this->set_log_append(); - $this->db = new DB; - } - - public function onLoad(): void{ - $lga = $this->get_log_append(); - $this->logger("{$lga} Loading userdetail plugin..."); - } - - public function onEnable(): void{ - - } - - public function onDisable(): void{ - - } - - public function load_data(){ - $this->data = $this->unmemorize_plugin("userdetail"); - } - - public function save_data(){ - $this->memorize_plugin("userdetail"); - } - - public function saveDataDisk(array $payload = null){ - $this->logger("[usercount] Saving data to disk..."); - if($payload == null){ - $payload = $this->additional_payload; - } - $handle = fopen($_SERVER["DOCUMENT_ROOT"] . parent::get_basepath() . "/data/userdetail.json", "w+"); - if(fwrite($handle, json_encode($payload))){ - fclose($handle); - return true; - } - } - - public function getDataDisk(){ - $this->logger("[usercount] Getting data from disk..."); - return json_decode(file_get_contents($_SERVER["DOCUMENT_ROOT"] . parent::get_basepath() . "/data/userdetail.json"), true); - } - - public function get_users(){ - $this->logger("[usercount] Getting user count..."); - - try { - $sql = "SELECT COUNT(*) FROM `users`;"; - $result = $this->db->sendQuery($sql)->execute(); - - $r = $result->fetch(\PDO::FETCH_ASSOC); - - return $r[0]; - } catch (\Exception $e){ - throw new \Exception((string)$e); - } - } -} - - -?> \ No newline at end of file diff --git a/api/v1/class/plugins/plugins/usercount/views/test.php b/api/v1/class/plugins/plugins/usercount/views/test.php deleted file mode 100644 index e273338..0000000 --- a/api/v1/class/plugins/plugins/usercount/views/test.php +++ /dev/null @@ -1,12 +0,0 @@ -get_users(); -?> -

Amount of users:

\ No newline at end of file From 6bf7b093c969843b055a46766a1be5541549719c Mon Sep 17 00:00:00 2001 From: Ente Date: Thu, 20 Feb 2025 13:41:51 +0100 Subject: [PATCH 04/48] TT-165: Fix typo in userdetail plugin preventing save --- api/v1/class/plugins/plugins/userdetail/views/user.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/v1/class/plugins/plugins/userdetail/views/user.php b/api/v1/class/plugins/plugins/userdetail/views/user.php index 599a872..c065035 100644 --- a/api/v1/class/plugins/plugins/userdetail/views/user.php +++ b/api/v1/class/plugins/plugins/userdetail/views/user.php @@ -14,7 +14,7 @@ $nav = $main->compute_user_nav(); $user = $benutzer->get_user($_GET["user"]); -$id = filter_var($_POS["id"], FILTER_SANITIZE_NUMBER_INT); +$id = filter_var($_POST["id"], FILTER_SANITIZE_NUMBER_INT); if($id){ $payload = [ From af4c8f4630c570d9c0a4d9ac299426a492926a59 Mon Sep 17 00:00:00 2001 From: Ente Date: Thu, 20 Feb 2025 13:54:48 +0100 Subject: [PATCH 05/48] TT-166: Redirect when calendar ID is not found --- api/v1/class/arbeitszeit.inc.php | 3 +++ api/v1/class/i18n/suite/status/snippets_DE.json | 3 ++- api/v1/class/i18n/suite/status/snippets_EN.json | 3 ++- api/v1/class/i18n/suite/status/snippets_NL.json | 3 ++- suite/notifications/view.php | 6 ++++++ 5 files changed, 15 insertions(+), 3 deletions(-) diff --git a/api/v1/class/arbeitszeit.inc.php b/api/v1/class/arbeitszeit.inc.php index e70b7f9..a8d1330 100644 --- a/api/v1/class/arbeitszeit.inc.php +++ b/api/v1/class/arbeitszeit.inc.php @@ -628,6 +628,9 @@ public static function check_status_code($url) if (strpos($url, "info=error")) { return "

{$loc["error"]}

"; } + if (strpos($url, "info=notification_not_found")) { + return "

{$loc["notification_not_found"]}

"; + } } diff --git a/api/v1/class/i18n/suite/status/snippets_DE.json b/api/v1/class/i18n/suite/status/snippets_DE.json index 3cc1e08..1fd374c 100644 --- a/api/v1/class/i18n/suite/status/snippets_DE.json +++ b/api/v1/class/i18n/suite/status/snippets_DE.json @@ -25,5 +25,6 @@ "error": "Hinweis: Ein Fehler ist aufgetreten. Bitte überprüfe den Log, falls möglich.", "statemismatch": "Fehler: Sicherheitsfehler", "ldapauth": "Fehler: Die Authentifizierung über LDAP ist fehlgeschlagen.", - "ldapcreated": "Hinweis: Bitte melde dich erneut an. Dein Benutzerkonto wurde nun erstellt. (LDAP allowed self-login)" + "ldapcreated": "Hinweis: Bitte melde dich erneut an. Dein Benutzerkonto wurde nun erstellt. (LDAP allowed self-login)", + "notification_not_found": "Fehler: Benachrichtigung nicht gefunden." } \ No newline at end of file diff --git a/api/v1/class/i18n/suite/status/snippets_EN.json b/api/v1/class/i18n/suite/status/snippets_EN.json index 7d01ccb..319c372 100644 --- a/api/v1/class/i18n/suite/status/snippets_EN.json +++ b/api/v1/class/i18n/suite/status/snippets_EN.json @@ -25,6 +25,7 @@ "error": "Note: An error has occurred. Please check the log if possible.", "statemismatch": "Error: Security error", "ldapauth": "Error: The authentication via LDAP failed!", - "ldapcreated": "Note: Please login again. Your account has just been created. (LDAP allowed self-login)" + "ldapcreated": "Note: Please login again. Your account has just been created. (LDAP allowed self-login)", + "notification_not_found": "Error: Notification not found." } \ No newline at end of file diff --git a/api/v1/class/i18n/suite/status/snippets_NL.json b/api/v1/class/i18n/suite/status/snippets_NL.json index 576e8bb..4a9d771 100644 --- a/api/v1/class/i18n/suite/status/snippets_NL.json +++ b/api/v1/class/i18n/suite/status/snippets_NL.json @@ -25,5 +25,6 @@ "error": "Opmerking: er is een fout opgetreden. Controleer indien mogelijk het logboek.", "statemismatch": "Fout: beveiligingsfout", "ldapauth": "Fout: Authenticatie via LDAP mislukt.", - "ldapcreated": "Opmerking: log opnieuw in. Uw gebruikersaccount is nu aangemaakt. (LDAP staat zelfinloggen toe)" + "ldapcreated": "Opmerking: log opnieuw in. Uw gebruikersaccount is nu aangemaakt. (LDAP staat zelfinloggen toe)", + "notification_not_found": "Fout: melding niet gevonden." } \ No newline at end of file diff --git a/suite/notifications/view.php b/suite/notifications/view.php index 836d63b..4b19e51 100644 --- a/suite/notifications/view.php +++ b/suite/notifications/view.php @@ -6,6 +6,12 @@ $arbeit->auth()->login_validation(); $id = $_GET["id"]; $data = $arbeit->notifications()->get_notifications_entry($id); + +if(isset($data["error"])){ + header("Location: /suite/?info=notification_not_found"); + exit; +} + $loc = $arbeit->i18n()->loadLanguage(null, "notifications/view"); $iid = htmlspecialchars($id); ?> From 7b221a6808f2cbbf7893a551933d0d145d477815 Mon Sep 17 00:00:00 2001 From: Ente Date: Thu, 20 Feb 2025 14:58:34 +0100 Subject: [PATCH 06/48] TT-156: Remove webedit for app.json --- api/v1/class/arbeitszeit.inc.php | 48 --------------------- suite/admin/actions/users/edit_settings.php | 26 ----------- suite/admin/users/settings.php | 11 ----- 3 files changed, 85 deletions(-) delete mode 100644 suite/admin/actions/users/edit_settings.php diff --git a/api/v1/class/arbeitszeit.inc.php b/api/v1/class/arbeitszeit.inc.php index a8d1330..c673d75 100644 --- a/api/v1/class/arbeitszeit.inc.php +++ b/api/v1/class/arbeitszeit.inc.php @@ -655,54 +655,6 @@ public function calculate_hours_specific_time($username, $month, $year) } } - /** - * change_settings() - Allows you to change specific settings in the app.ini - * - * @param array $array Contains the array with changing values - * @return bool Returns true o success and false otherwise - */ - public static function change_settings($array) - { - $ini = self::get_app_ini(); - foreach ($array as $key => $value) { - unset($ini["general"][(string) $key]); - $ini["general"][(string) $key] = $value; - - } - Exceptions::error_rep("Changing settings..."); - $file = fopen($_SERVER["DOCUMENT_ROOT"] . "/api/v1/inc/app.ini", "w"); - $cini = self::arr2ini($ini); - if (fwrite($file, $cini)) { - fclose($file); - return true; - } else { - Exceptions::error_rep("An error occured while chaning settings"); - fclose($file); - return false; - } - } - - private static function arr2ini(array $a, array $parent = array()) - { - Exceptions::error_rep("Writing to app.ini..."); - $out = ''; - foreach ($a as $k => $v) { - if (is_array($v)) { - //subsection case - //merge all the sections into one array... - $sec = array_merge((array) $parent, (array) $k); - //add section information to the output - $out .= '[' . join('.', $sec) . ']' . PHP_EOL; - //recursively traverse deeper - $out .= self::arr2ini($v, $sec); - } else { - //plain key->value case - $out .= "$k=\"$v\"" . PHP_EOL; - } - } - return $out; - } - public static function get_worktime_by_id($id) { $conn = new DB; diff --git a/suite/admin/actions/users/edit_settings.php b/suite/admin/actions/users/edit_settings.php deleted file mode 100644 index 2069de2..0000000 --- a/suite/admin/actions/users/edit_settings.php +++ /dev/null @@ -1,26 +0,0 @@ -get_app_ini(); -$base_url = $ini["general"]["base_url"]; -$arbeit->auth()->login_validation(); -if($arbeit->benutzer()->is_admin($arbeit->benutzer()->get_user($_SESSION["username"]))){ - # build array - if(isset($_POST["app_name"])){ - $settings["app_name"] = $_POST["app_name"]; - } - if(isset($_POST["base_url"])){ - $settings["base_url"] = $_POST["base_url"]; - } - if(!isset($settings)){ - header("Location: http://{$base_url}/suite/?info=error"); - } - if($arbeit->change_settings($settings) == true){ - header("Location: http://{$base_url}/suite/?info=settings_changed"); - } -} else { - header("Location: http://{$base_url}/suite/?info=noperms"); -} -?> \ No newline at end of file diff --git a/suite/admin/users/settings.php b/suite/admin/users/settings.php index 4328552..60e5f62 100644 --- a/suite/admin/users/settings.php +++ b/suite/admin/users/settings.php @@ -12,17 +12,6 @@ ?>
-

- -

- -
- " value=""> -
- " value=""> -
- -
Date: Thu, 20 Feb 2025 15:08:19 +0100 Subject: [PATCH 07/48] TT-164: Display warning before changing username --- .../class/plugins/plugins/userdetail/views/user.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/api/v1/class/plugins/plugins/userdetail/views/user.php b/api/v1/class/plugins/plugins/userdetail/views/user.php index c065035..fddc98c 100644 --- a/api/v1/class/plugins/plugins/userdetail/views/user.php +++ b/api/v1/class/plugins/plugins/userdetail/views/user.php @@ -49,7 +49,7 @@

| Userdetail

-
&id=" method="POST"> + &id=" method="POST"> ">


@@ -63,3 +63,14 @@
+ + From 66c0e8fbac8acaf826d1560ff51eb989f45f7029 Mon Sep 17 00:00:00 2001 From: Ente Date: Thu, 20 Feb 2025 15:14:11 +0100 Subject: [PATCH 08/48] TT-164: Prevent userdetail plugin from crashing when accessing non-existent user. --- api/v1/class/plugins/plugins/userdetail/src/Main.php | 6 +++++- api/v1/class/plugins/plugins/userdetail/views/user.php | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/api/v1/class/plugins/plugins/userdetail/src/Main.php b/api/v1/class/plugins/plugins/userdetail/src/Main.php index ecb2f64..4cd7940 100644 --- a/api/v1/class/plugins/plugins/userdetail/src/Main.php +++ b/api/v1/class/plugins/plugins/userdetail/src/Main.php @@ -117,7 +117,11 @@ public function save_employee_data($payload){ public function get_employee_data($username){ $this->logger("[userdetail] Getting employee data for employee: {$username}..."); - if(json_decode(@file_get_contents(dirname(__DIR__, 1) . "/data/" . $username . ".json"), true) != false){ + $path = @file_get_contents(dirname(__DIR__, 1) . "/data/" . $username . ".json"); + if($path == null || $path == false){ + header("Location: /suite/plugins/index.php?pn=userdetail&p_view=views/user.php&user={$username}&nuser=true"); + } + if(json_decode($path, true) != false){ return json_decode(file_get_contents(dirname(__DIR__, 1) . "/data/" . $username . ".json"), true); } else { $this->logger("[userdetail] No data found for employee: {$username}..."); diff --git a/api/v1/class/plugins/plugins/userdetail/views/user.php b/api/v1/class/plugins/plugins/userdetail/views/user.php index fddc98c..c507c54 100644 --- a/api/v1/class/plugins/plugins/userdetail/views/user.php +++ b/api/v1/class/plugins/plugins/userdetail/views/user.php @@ -16,6 +16,11 @@ $id = filter_var($_POST["id"], FILTER_SANITIZE_NUMBER_INT); +if(isset($_GET["nuser"])){ + echo "

User not found.

"; + die(); +} + if($id){ $payload = [ "id" => $id, From 476251d7670591027bce4bdeb5bb986316fc1ba6 Mon Sep 17 00:00:00 2001 From: Ente Date: Thu, 20 Feb 2025 15:20:39 +0100 Subject: [PATCH 09/48] Adjust versions / CHANGELOG.md --- CHANGELOG.md | 11 +++++++++++ VERSION | 2 +- api/v1/class/plugins/plugins/userdetail/plugin.yml | 2 +- composer.json | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bca98a0..2707804 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # CHANGELOG +## v7.10 + +* Reflected changes from v7.9 release into Mails.md +* You now get redirected when calendar ID is not found +* Fixed being unable to access the "Forgot password" page +* Fixed typo in userdetail plugin preventing save +* Remove usercount plugin entirely +* Removed webedit for app.json +* A warning is now displayed when an admin changes user information within the userdetail plugin +* Fixed an bug causing userdetail plugin to crash when the selected user could not be found + ## v7.9 * Fixed being unable to access "userdetail" plugin diff --git a/VERSION b/VERSION index c8357ab..b692764 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.9 \ No newline at end of file +7.10 \ No newline at end of file diff --git a/api/v1/class/plugins/plugins/userdetail/plugin.yml b/api/v1/class/plugins/plugins/userdetail/plugin.yml index 090477c..21ba4bb 100644 --- a/api/v1/class/plugins/plugins/userdetail/plugin.yml +++ b/api/v1/class/plugins/plugins/userdetail/plugin.yml @@ -4,7 +4,7 @@ main: Main namespace: userdetail author: Ente description: Save more information about your employees -version: "1.1.1" +version: "1.1.2" api: 0.1 permissions: none enabled: true diff --git a/composer.json b/composer.json index 207b473..92e4a6d 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "TimeTrack is a PHP-written time recording tool for small businesses", "type": "software", "license": "GNU GPL", - "version": "7.9", + "version": "7.10", "authors": [ { "name": "Ente", From 02dab51d1ce6c108d73ebbd203c1749949cd809a Mon Sep 17 00:00:00 2001 From: Ente Date: Thu, 20 Feb 2025 16:07:44 +0100 Subject: [PATCH 10/48] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d6d5d3c..b10ce88 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -34,5 +34,7 @@ If applicable, add screenshots to help explain your problem. - Browser [e.g. stock browser, safari] - Version [e.g. 22] +**TimeTrack Version**: e.g. v7.9 + **Additional context** Add any other context about the problem here. From 268a1742532eebb141307e8cf18154bbc1824df9 Mon Sep 17 00:00:00 2001 From: Ente Date: Sat, 1 Mar 2025 20:27:35 +0100 Subject: [PATCH 11/48] TT-170: Add missing CSS to elements --- CHANGELOG.md | 4 ++++ VERSION | 2 +- api/v1/class/mode/mode.arbeit.inc.php | 2 +- composer.json | 2 +- suite/actions/auth/reset.php | 4 ++-- suite/admin/users/edit.php | 2 +- suite/admin/worktime/all.php | 4 ++-- suite/forgot_password.php | 2 +- suite/worktime/sick/index.php | 4 ++-- suite/worktime/vacation/index.php | 4 ++-- 10 files changed, 17 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2707804..473fd26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## v7.10.1 + +* Added some CSS to certain elements which were missing it + ## v7.10 * Reflected changes from v7.9 release into Mails.md diff --git a/VERSION b/VERSION index b692764..202d1aa 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.10 \ No newline at end of file +7.10.1 \ No newline at end of file diff --git a/api/v1/class/mode/mode.arbeit.inc.php b/api/v1/class/mode/mode.arbeit.inc.php index 92da39a..9c11000 100644 --- a/api/v1/class/mode/mode.arbeit.inc.php +++ b/api/v1/class/mode/mode.arbeit.inc.php @@ -23,7 +23,7 @@ private static function get_normal_mode_html(){ $data = <<< DATA
- +
diff --git a/composer.json b/composer.json index 92e4a6d..7db2c4a 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "TimeTrack is a PHP-written time recording tool for small businesses", "type": "software", "license": "GNU GPL", - "version": "7.10", + "version": "7.10.1", "authors": [ { "name": "Ente", diff --git a/suite/actions/auth/reset.php b/suite/actions/auth/reset.php index 1cfd4ed..ba4a836 100644 --- a/suite/actions/auth/reset.php +++ b/suite/actions/auth/reset.php @@ -56,10 +56,10 @@

Password forgot?

Please enter your new password in the below's form.

You will recieve a email after submiting your new password.

- + " hidden>
- +
\ No newline at end of file diff --git a/suite/admin/users/edit.php b/suite/admin/users/edit.php index 861824d..5bfafc6 100644 --- a/suite/admin/users/edit.php +++ b/suite/admin/users/edit.php @@ -42,7 +42,7 @@

- +

diff --git a/suite/admin/worktime/all.php b/suite/admin/worktime/all.php index 879acdc..8252d16 100644 --- a/suite/admin/worktime/all.php +++ b/suite/admin/worktime/all.php @@ -33,8 +33,8 @@

" method="POST">

- - + +
diff --git a/suite/forgot_password.php b/suite/forgot_password.php index 3353b68..db5688d 100644 --- a/suite/forgot_password.php +++ b/suite/forgot_password.php @@ -18,7 +18,7 @@

- +
diff --git a/suite/worktime/sick/index.php b/suite/worktime/sick/index.php index 68d4ed6..47998b6 100644 --- a/suite/worktime/sick/index.php +++ b/suite/worktime/sick/index.php @@ -21,10 +21,10 @@

- +

:

- +
diff --git a/suite/worktime/vacation/index.php b/suite/worktime/vacation/index.php index 8d33a85..36550b5 100644 --- a/suite/worktime/vacation/index.php +++ b/suite/worktime/vacation/index.php @@ -23,10 +23,10 @@

- +

:

- +
From 52404c1aacc887fd982c8c39aa68d5766526d45c Mon Sep 17 00:00:00 2001 From: Ente Date: Sat, 1 Mar 2025 20:57:27 +0100 Subject: [PATCH 12/48] TT-170: Hotfix for adding worktime in normal mode --- suite/actions/worktime/add.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/suite/actions/worktime/add.php b/suite/actions/worktime/add.php index 7e526e4..8268857 100644 --- a/suite/actions/worktime/add.php +++ b/suite/actions/worktime/add.php @@ -2,8 +2,8 @@ require $_SERVER["DOCUMENT_ROOT"] . "/api/v1/inc/arbeit.inc.php"; use Arbeitszeit\Arbeitszeit; $worktime = new Arbeitszeit; -$base_url = $arbeit->get_app_ini()["general"]["base_url"]; -$arbeit->auth()->login_validation(); +$base_url = $worktime->get_app_ini()["general"]["base_url"]; +$worktime->auth()->login_validation(); $work = $worktime->add_worktime($_POST["time_start"], $_POST["time_end"], $_POST["ort"], $_POST["date"], $_POST["username"],"worktime", array("start" => $_POST["pause_start"], "end" => $_POST["pause_end"])); if(!$work){ header("Location: http://{$base_url}/suite/?info=error_worktime"); From 58210d9a4fdea795be8c24b72e0cd80cc670c1e4 Mon Sep 17 00:00:00 2001 From: Ente Date: Fri, 14 Mar 2025 19:41:15 +0100 Subject: [PATCH 13/48] TT-171: Small fixes --- README.md | 15 +++++++++------ api/v1/class/sickness/sickness.arbeit.inc.php | 4 ++-- api/v1/class/vacation/vacation.arbeit.inc.php | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 5cb11cd..d1e8e66 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Additional functionality can be unlocked with TimeTrack Oval ### Requirements -- at least PHP 8.2 (`curl|gd|gmp|intl|mbstring|mysqli|openssl|pgsql|xsl|gettext|dom|ldap`) +- at least PHP 8.2 (`curl|gd|gmp|intl|mbstring|mysqli|openssl|xsl|gettext|dom|ldap`) - composer (to install dependencies; phpmailer: for sending emails via smtp, parsedown: markdown parser for the `CHANGELOG.md`, simple-router: does the API routing, yaml: for reading plugin yaml files, ldaptools: for LDAP authentication, dompdf: for PDF generation) - Apache2.4 with enabled rewrite mod (optional) @@ -83,10 +83,16 @@ LDAP authentication works with OpenLDAP and Active Directory. - `ldap_ip`: IP address of your LDAP server (e.g. `1.1.1.1`) - `ldap_domain`: The domain your LDAP server controls (e.g. `example.local`) - `ldap_basedn`: Base DN for your domain (e.g. `dc=example,dc=local`) -- `ldap_group`: Group membership required by LDAP users to be able to authenticate +- `ldap_group`: Group membership required by LDAP users to be able to authenticate (e.g. `Domain Users`, (new group) `TimeTrack Users`) - `saf`: Specify if you only have one LDAP server (true) or another one as fallback (false) - `saf_*`: If `saf` is set to `false`, please specify the corresponding values to the `saf_*` configuration -- `create_user`: If set to `true` it creates an user account automatically if the desired account is authenticated and within specified group. If set to `false` login simply fails, even if authenticated +- `create_user`: If set to `true` it creates an user account automatically if the desired account is authenticated and within specified group. If set to `false` login simply fails, even if authenticated. + +#### **Export** + +##### **PDF** + +- `css`: Full path to the CSS file used for the PDF export (default: `api/v1/class/exports/modules/PDFExportModule/css/index.css`) - **optional value** If done correctly, you should now be able to access the application via http://BASE_URL/ - redirects to http://BASE_URL/suite/ @@ -125,7 +131,6 @@ Already existing local accounts will get their authentication overwritten if an In order to create accounts automatically if `create_user` is `true` make sure to set the user's email address! Otherwise login fails. If above mentioned setting is set to `false` you have to create a user on your own locally and then let the user login with their LDAP credentials. The credentials you have entered will become usable if you disable LDAP or rename the account on your LDAP server. -Please run `run-patch.sh` within the `setup` folder to get LDAP working with php >8.0 ## Export @@ -186,5 +191,3 @@ You can update the database by downloading the `setup/upgrade.php` file into you From here on just edit the `$missingUpdate` variable to the desired version as specified. Please be aware that you are not able to skip an database update. You have to update one by one, e.g. from 1 -> 2, 2 -> 3, ... - - diff --git a/api/v1/class/sickness/sickness.arbeit.inc.php b/api/v1/class/sickness/sickness.arbeit.inc.php index 4425fa9..20e3a5d 100644 --- a/api/v1/class/sickness/sickness.arbeit.inc.php +++ b/api/v1/class/sickness/sickness.arbeit.inc.php @@ -89,8 +89,8 @@ public function display_sickness_all(){ # admin function only case "approved": $status = "{$i18n["status"]["approved"]}"; break; - case "action": - $status = "Action needed"; + case "rejected": + $status = "{$i18n["status"]["rejected"]}"; break; } diff --git a/api/v1/class/vacation/vacation.arbeit.inc.php b/api/v1/class/vacation/vacation.arbeit.inc.php index 42fb69f..b301231 100644 --- a/api/v1/class/vacation/vacation.arbeit.inc.php +++ b/api/v1/class/vacation/vacation.arbeit.inc.php @@ -117,7 +117,7 @@ public function display_vacation_all(){ # admin function only $status = "{$i18n["status"]["approved"]}"; break; case "rejected": - $status = "{$i18n["status"]["rejected"]}"; + $status = "{$i18n["status"]["rejected"]}"; break; } From 579111407ad85808d67d11f0bb44c4e6589165e5 Mon Sep 17 00:00:00 2001 From: Ente Date: Fri, 28 Mar 2025 03:31:37 +0100 Subject: [PATCH 14/48] TT-173: Implement base functionality to change user properties --- CHANGELOG.md | 8 + VERSION | 2 +- api/v1/class/benutzer/benutzer.arbeit.inc.php | 145 ++++++++++++------ .../plugins/plugins/userdetail/plugin.yml | 2 +- .../plugins/plugins/userdetail/views/user.php | 69 ++++++--- composer.json | 2 +- 6 files changed, 157 insertions(+), 71 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 473fd26..288547e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # CHANGELOG +## v7.10.2 + +* Added native function to `Benutzer` class to update user proprties which not lets the `userdetail` plugin actually update user properties + +## v7.10.1.1 + +* Hotfix preventing to add a worktime in normal mode + ## v7.10.1 * Added some CSS to certain elements which were missing it diff --git a/VERSION b/VERSION index 202d1aa..5a4adf1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.10.1 \ No newline at end of file +7.10.2 \ No newline at end of file diff --git a/api/v1/class/benutzer/benutzer.arbeit.inc.php b/api/v1/class/benutzer/benutzer.arbeit.inc.php index 9e02b93..5f83fff 100644 --- a/api/v1/class/benutzer/benutzer.arbeit.inc.php +++ b/api/v1/class/benutzer/benutzer.arbeit.inc.php @@ -1,11 +1,13 @@ db = $this->db(); $this->i18n = $this->i18n()->loadLanguage(null, "class/benutzer"); } @@ -17,24 +19,25 @@ public function __construct(){ * @param string $name First name of the employee * @return array|bool Returns true on success and an array otherwise */ - public function create_user($username, $name, $email, $password, $isAdmin = 0){ + public function create_user($username, $name, $email, $password, $isAdmin = 0) + { #$originalFunction = function($username, $name, $email, $password, $isAdmin){ - Exceptions::error_rep("Creating user '$username'..."); - $password = password_hash($password, PASSWORD_DEFAULT); - $sql = "INSERT INTO `users` (`name`, `username`, `email`, `password`, `email_confirmed`, `isAdmin`) VALUES (?, ?, ?, ?, '1', ?);"; - $data = $this->db->sendQuery($sql)->execute([$name, $username, $email, $password, $isAdmin]); - if($data == false){ - Exceptions::error_rep("An error occured while creating a user. See previous message for more information"); - return [ - "error" => [ - "error_code" => 3, - "error_message" => "Error while creating a user!" - ] - ]; - } else { - Exceptions::error_rep("User '$username' created successfully."); - return true; - } + Exceptions::error_rep("Creating user '$username'..."); + $password = password_hash($password, PASSWORD_DEFAULT); + $sql = "INSERT INTO `users` (`name`, `username`, `email`, `password`, `email_confirmed`, `isAdmin`) VALUES (?, ?, ?, ?, '1', ?);"; + $data = $this->db->sendQuery($sql)->execute([$name, $username, $email, $password, $isAdmin]); + if ($data == false) { + Exceptions::error_rep("An error occured while creating a user. See previous message for more information"); + return [ + "error" => [ + "error_code" => 3, + "error_message" => "Error while creating a user!" + ] + ]; + } else { + Exceptions::error_rep("User '$username' created successfully."); + return true; + } #}; #return Hooks::executeWithHooks('create_user', $originalFunction, $username, $name, $email, $password, $isAdmin); } @@ -47,11 +50,12 @@ public function create_user($username, $name, $email, $password, $isAdmin = 0){ * * @note This function only deletes the user but not their other data. */ - public function delete_user($id){ + public function delete_user($id) + { Exceptions::error_rep("Deleting user with id '$id'..."); $sql = "DELETE FROM `users` WHERE id = ?;"; $data = $this->db->sendQuery($sql)->execute([$id]); - if($data == false){ + if ($data == false) { Exceptions::error_rep("An error occured while deleting an user. See previous message for more information"); return [ "error" => [ @@ -70,14 +74,15 @@ public function delete_user($id){ * @param string $username * @return array|bool Returns false on failure and an array otherwise */ - public static function get_user($username){ + public static function get_user($username) + { Exceptions::error_rep("Getting user '$username'..."); $sql = "SELECT * FROM `users` WHERE username = ?;"; $db = new DB; $res = $db->sendQuery($sql); $res->execute([$username]); $count = $res->rowCount(); - if($count == 1){ + if ($count == 1) { $data = $res->fetch(\PDO::FETCH_ASSOC); Exceptions::error_rep("User '$username' found."); return $data; @@ -92,13 +97,14 @@ public static function get_user($username){ * @param int $id * @return array|bool Returns false on failure and an array otherwise */ - public static function get_user_from_id($id){ + public static function get_user_from_id($id) + { Exceptions::error_rep("Getting user with id '$id'..."); $conn = new DB(); $sql = "SELECT * FROM `users` WHERE id = ?;"; $res = $conn->sendQuery($sql); $res->execute([$id]); - if($res->rowCount() == 1){ + if ($res->rowCount() == 1) { $data = $res->fetch(\PDO::FETCH_ASSOC); Exceptions::error_rep("User with id '$id' found."); return $data; @@ -113,13 +119,14 @@ public static function get_user_from_id($id){ * @param string $email * @return array|bool Returns false on failure and an array otherwise */ - public static function get_user_from_email($email){ + public static function get_user_from_email($email) + { Exceptions::error_rep("Getting user with email '$email'..."); $conn = new DB(); $sql = "SELECT * FROM `users` WHERE email = ?;"; $res = $conn->sendQuery($sql); $res->execute([$email]); - if($res->rowCount() == 1){ + if ($res->rowCount() == 1) { $data = $res->fetch(\PDO::FETCH_ASSOC); Exceptions::error_rep("User with email '$email' found."); return $data; @@ -134,13 +141,14 @@ public static function get_user_from_email($email){ * @param string $email * @return string|bool Returns false on failure and a string otherwise */ - public function get_username_from_email($email){ + public function get_username_from_email($email) + { Exceptions::error_rep("Getting username with email '$email'..."); $conn = new DB(); $sql = "SELECT username FROM `users` WHERE email = ?;"; $res = $conn->sendQuery($sql); $res->execute([$email]); - if($res->rowCount() == 1){ + if ($res->rowCount() == 1) { $data = $res->fetch(\PDO::FETCH_ASSOC); Exceptions::error_rep("Username with email '$email' found."); return $data["username"]; @@ -154,15 +162,16 @@ public function get_username_from_email($email){ * get_all_users() - Gets all users from the database * @return array|bool Returns false on failure and an array otherwise */ - public function get_all_users(){ + public function get_all_users() + { Exceptions::error_rep("Getting all users..."); $sql = "SELECT * FROM `users`;"; $res = $this->db->sendQuery($sql); $res->execute(); $count = $res->rowCount(); $dat = []; - if($count >= 1){ - while($data = $res->fetch(\PDO::FETCH_ASSOC)){ + if ($count >= 1) { + while ($data = $res->fetch(\PDO::FETCH_ASSOC)) { $dat[$data["id"]] = $data; } Exceptions::error_rep("Users found and returing data."); @@ -177,7 +186,8 @@ public function get_all_users(){ * get_all_users_html() - Gets all users from the database * @return string Returns a string (rendered HTML) on success */ - public function get_all_users_html(){ + public function get_all_users_html() + { Exceptions::error_rep("Getting all users..."); $base_url = $ini = $this->get_app_ini()["general"]["base_url"]; $sql = "SELECT * FROM `users`;"; @@ -185,12 +195,12 @@ public function get_all_users_html(){ $res->execute(); $count = $res->rowCount(); - if($count == 0){ + if ($count == 0) { Exceptions::failure("ERR-NO-USERS", "No users found?", "N/A"); return "

{$this->i18n["no_users"]}

"; } - while($data = $res->fetch(\PDO::FETCH_ASSOC)){ - if($data["username"] == "api"){ + while ($data = $res->fetch(\PDO::FETCH_ASSOC)) { + if ($data["username"] == "api") { $data["email"] = "System user"; } @@ -199,7 +209,7 @@ public function get_all_users_html(){ $username = $data["username"]; $id = $data["id"]; - $html = <<< DOC + $html = << {$this->i18n["delete_user"]} | {$this->i18n["edit_user"]} $name @@ -220,21 +230,22 @@ public function get_all_users_html(){ * @param string $username * @return string Returns a string (rendered HTML) on success */ - public function get_user_html($username){ + public function get_user_html($username) + { Exceptions::error_rep("Getting user '$username'..."); $base_url = $ini = $this->get_app_ini()["general"]["base_url"]; $data = $this->get_user($username); - if($data == false){ + if ($data == false) { Exceptions::error_rep("An error occured while generating user html. User '$username' is either not logged in or the session expired."); return "

{$this->i18n["unknown_error"]}

"; } - while($data){ + while ($data) { $reww = $data["name"]; $rol = $data["id"]; $ral = $data["email"]; $rww = $data["username"]; - $html = <<< DATA + $html = <<

{$this->i18n["name"]}: {$reww}

@@ -259,10 +270,56 @@ public function get_user_html($username){ * @param array $user * @return bool Returns true if the user is an admin and false otherwise */ - public function is_admin($user){ - if($user["isAdmin"] == true){ + public function is_admin($user) + { + if ($user["isAdmin"] == true) { + return true; + } else { + return false; + } + } + + public function editUserProperties(mixed $username_or_id, string $name, mixed $value): bool + { + if ( + !isset($_SESSION["username"]) || + !$this->is_admin($this->get_user($_SESSION["username"])) + ) { + Exceptions::error_rep("Unauthorized attempt by user '{$_SESSION["username"]}' to update user properties!"); + return false; + } + + $allowed_types = ["username", "email", "isAdmin", "name"]; + if (!in_array($name, $allowed_types)) { + Exceptions::error_rep("Could not update user entry – invalid property '{$name}'"); + return false; + } + + if (is_int($username_or_id)) { + $search_type = "id"; + } elseif (is_string($username_or_id)) { + $search_type = "username"; + } else { + Exceptions::error_rep("Invalid identifier type for user update."); + return false; + } + + Exceptions::error_rep("Updating user entry '{$username_or_id}' (Type: {$search_type}); Property: {$name}; Value: {$value}"); + + $sql = "UPDATE `users` SET `$name` = ? WHERE `$search_type` = ?"; + try { + $stmt = $this->db->sendQuery($sql); + $success = $stmt->execute([$value, $username_or_id]); + } catch (\Exception $e) { + Exceptions::error_rep("Database exception while updating user: " . $e->getMessage()); + return false; + } + + if ($success) { + Exceptions::error_rep("Successfully updated user entry '{$username_or_id}' (Type: {$search_type})"); return true; } else { + Exceptions::error_rep("Update failed for user '{$username_or_id}' (Type: {$search_type})"); return false; } } diff --git a/api/v1/class/plugins/plugins/userdetail/plugin.yml b/api/v1/class/plugins/plugins/userdetail/plugin.yml index 21ba4bb..b860420 100644 --- a/api/v1/class/plugins/plugins/userdetail/plugin.yml +++ b/api/v1/class/plugins/plugins/userdetail/plugin.yml @@ -4,7 +4,7 @@ main: Main namespace: userdetail author: Ente description: Save more information about your employees -version: "1.1.2" +version: "1.2" api: 0.1 permissions: none enabled: true diff --git a/api/v1/class/plugins/plugins/userdetail/views/user.php b/api/v1/class/plugins/plugins/userdetail/views/user.php index c507c54..3da1e98 100644 --- a/api/v1/class/plugins/plugins/userdetail/views/user.php +++ b/api/v1/class/plugins/plugins/userdetail/views/user.php @@ -14,40 +14,60 @@ $nav = $main->compute_user_nav(); $user = $benutzer->get_user($_GET["user"]); -$id = filter_var($_POST["id"], FILTER_SANITIZE_NUMBER_INT); - -if(isset($_GET["nuser"])){ +if (isset($_GET["nuser"])) { echo "

User not found.

"; die(); } -if($id){ - $payload = [ - "id" => $id, - "username" => $_POST["username"], - "notes" => $_POST["notes"], - "position" => $_POST["position"], - "employee-id" => $_POST["employee-id"], - "department" => $_POST["department"], - "email" => $_POST["email"], - ]; - if($main->save_employee_data($payload)){ - echo "Successfully saved data for {$payload["username"]}
"; +if ($_SERVER["REQUEST_METHOD"] === "POST") { + $id = $user["id"]; + $username = $_POST["username"]; + $email = $_POST["email"]; + $name = $_POST["name"]; + + if (!empty($username)) { + if ($benutzer->editUserProperties($id, "username", $username)) { + echo "Updated username for ID {$id} (If changed - Please reload the page again to reload the new properties.).
"; + } } - if($_POST["reset-password"] == true || $_POST["reset-password"] == "on"){ - if($auth->reset_password($_POST["username"])){ - echo "Successfully reset password for {$payload["username"]}"; + if (!empty($_POST["name"])) { + if ($benutzer->editUserProperties($id, "name", $_POST["name"])) { + echo "Updated name for ID {$id} (If changed).
"; } } -} + if (!empty($email)) { + if ($benutzer->editUserProperties($id, "email", $email)) { + echo "Updated email for ID {$id} (If changed).
"; + } + } -if($r = $main->get_employee_data($_GET["user"])){ - $notes = $r["notes"] ?? ""; - $pos = $r["position"] ?? ""; - $eid = $r["employee-id"] ?? ""; - $department = $r["department"] ?? ""; + if (!empty($_POST["reset-password"])) { + if ($auth->reset_password($username)) { + echo "Successfully reset password for {$username}
"; + } + } + + $payload = [ + "id" => $id, + "username" => $username, + "notes" => $_POST["notes"] ?? null, + "position" => $_POST["position"] ?? null, + "employee-id" => $_POST["employee-id"] ?? null, + "department" => $_POST["department"] ?? null, + "email" => $email, + "name" => $_POST["name"] ?? null, + ]; + if ($main->save_employee_data($payload)) { + echo "Successfully saved employee data for {$username}
"; + } } + +$r = $main->get_employee_data($_GET["user"]); +$notes = $r["notes"] ?? ""; +$pos = $r["position"] ?? ""; +$eid = $r["employee-id"] ?? ""; +$department = $r["department"] ?? ""; ?> @@ -56,6 +76,7 @@
&id=" method="POST"> ">
+ " placeholder="John Doe">



diff --git a/composer.json b/composer.json index 7db2c4a..12d80ee 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "TimeTrack is a PHP-written time recording tool for small businesses", "type": "software", "license": "GNU GPL", - "version": "7.10.1", + "version": "7.10.2", "authors": [ { "name": "Ente", From 78307161f8077b6ebee7b58b33c0bf7845d41d06 Mon Sep 17 00:00:00 2001 From: Ente Date: Fri, 28 Mar 2025 03:55:39 +0100 Subject: [PATCH 15/48] TT-174: Fix being unable to set worktime as uncompliant --- CHANGELOG.md | 1 + .../templates/WorktimeUncompliantTemplate.mails.arbeit.inc.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 288547e..917cbe8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## v7.10.2 * Added native function to `Benutzer` class to update user proprties which not lets the `userdetail` plugin actually update user properties +* Fixed bug ## v7.10.1.1 diff --git a/api/v1/class/mails/templates/WorktimeUncompliantTemplate.mails.arbeit.inc.php b/api/v1/class/mails/templates/WorktimeUncompliantTemplate.mails.arbeit.inc.php index d155f97..92afc06 100644 --- a/api/v1/class/mails/templates/WorktimeUncompliantTemplate.mails.arbeit.inc.php +++ b/api/v1/class/mails/templates/WorktimeUncompliantTemplate.mails.arbeit.inc.php @@ -18,7 +18,7 @@ public function render(array $data): MailTemplateData $conn = $arbeit->db(); $sql = "SELECT * FROM `users` WHERE username = ?;"; $res = $conn->sendQuery($sql); - $res->execute($data["username"]); + $res->execute([$data["username"]]); $count = $res->rowCount(); $ii = Arbeitszeit::get_app_ini()["general"]["app_name"]; From 4fff8313c08fe3789f684bad6e9cd7a7daedc5e1 Mon Sep 17 00:00:00 2001 From: Ente Date: Fri, 28 Mar 2025 04:11:36 +0100 Subject: [PATCH 16/48] TT-174: Fix accidental overwrite of sickness end dates --- api/v1/class/sickness/sickness.arbeit.inc.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/v1/class/sickness/sickness.arbeit.inc.php b/api/v1/class/sickness/sickness.arbeit.inc.php index 20e3a5d..440d659 100644 --- a/api/v1/class/sickness/sickness.arbeit.inc.php +++ b/api/v1/class/sickness/sickness.arbeit.inc.php @@ -75,7 +75,7 @@ public function display_sickness_all(){ # admin function only if($count > 0){ # compute and return data - while($row = $data->fetch(\PDO::FETCH_ASSOC)){ + foreach($data->fetchAll(\PDO::FETCH_ASSOC) as $row){ $rnw = $row["username"]; $start = @strftime("%d.%m.%Y", strtotime($row["start"])); $stop = @strftime("%d.%m.%Y", strtotime($row["stop"])); @@ -94,7 +94,7 @@ public function display_sickness_all(){ # admin function only break; } - if($stop = "01.01.1970"){ + if($stop == "01.01.1970"){ $stop = "-"; } From 9ac7cf5271188f1ecc94de836b709691d976c1ba Mon Sep 17 00:00:00 2001 From: Ente Date: Fri, 28 Mar 2025 13:29:35 +0100 Subject: [PATCH 17/48] TT-85: Differentiate API endpoints between admin, internal and public --- .../Permissions.routes.toil.arbeit.inc.php | 39 +++++--- api/v1/toil/Routes.toil.arbeit.inc.php | 99 +++++++++++-------- api/v1/toil/VERSION | 2 +- api/v1/toil/permissions.json | 6 +- 4 files changed, 88 insertions(+), 58 deletions(-) diff --git a/api/v1/toil/Permissions.routes.toil.arbeit.inc.php b/api/v1/toil/Permissions.routes.toil.arbeit.inc.php index f58e9c3..829efd8 100644 --- a/api/v1/toil/Permissions.routes.toil.arbeit.inc.php +++ b/api/v1/toil/Permissions.routes.toil.arbeit.inc.php @@ -43,23 +43,34 @@ private function checkUserPermission($username){ } - public function checkPermissions($user, $endpoint){ - $endpointP = @$this->loadPermissionSet()[$endpoint]; - if($endpointP === null) {$endpointP = "?"; Exceptions::error_rep("[API] Failed to check for permissions on endpoint: " . $endpoint . " (req: ". $endpointP . ", for user " . $user . ")"); return false;} - $perm = $this->checkUserPermission($user); - switch ($perm) { - case 1: - case $endpointP === 0: - return true; - case $perm == $endpointP: - return true; - default: - $endpointP = "?"; - Exceptions::error_rep("[API] Failed to check for permissions on endpoint: " . $endpoint . " (req: ". $endpointP . ", for user " . $user . ")"); - return false; + public function checkPermissions($user, $endpoint) { + $permissionsSet = $this->__get("permissions"); + $endpointPermission = $permissionsSet[$endpoint] ?? null; + + if ($endpointPermission === null) { + Exceptions::error_rep("[API] No permissions defined for endpoint: '$endpoint'"); + return false; + } + + if ($endpointPermission === 2) { + Exceptions::error_rep("[API] Public access granted for endpoint '$endpoint'"); + return true; + } + $userIsAdmin = $this->checkUserPermission($user); + + if ($endpointPermission === 1 && $userIsAdmin) { + return true; } + + if ($endpointPermission === 0 && !$userIsAdmin) { + return true; + } + + Exceptions::error_rep("[API] Permission denied for user '$user' on endpoint '$endpoint' (required: $endpointPermission, isAdmin: $userIsAdmin)"); + return false; } + } } diff --git a/api/v1/toil/Routes.toil.arbeit.inc.php b/api/v1/toil/Routes.toil.arbeit.inc.php index d2938d9..bc497fb 100644 --- a/api/v1/toil/Routes.toil.arbeit.inc.php +++ b/api/v1/toil/Routes.toil.arbeit.inc.php @@ -15,7 +15,8 @@ use Toil\Controller; use Toil\Permissions; -class Routes extends Toil { +class Routes extends Toil +{ private Arbeitszeit $arbeitszeit; private Benutzer $benutzer; @@ -29,9 +30,22 @@ class Routes extends Toil { private $bString = "Toil API"; - public function __construct(){ - $user = $_SERVER["PHP_AUTH_USER"]; - $pw = $_SERVER["PHP_AUTH_PW"]; + public function __construct() + { + $this->__set("eventHandler", new EventHandler()); + + $endpoint = preg_replace('/\?.*/', '', $_SERVER['REQUEST_URI']); + $endpointName = $this->getResourceNameFromPath($endpoint); + $permissions = new Permissions(); + + if ($permissions->checkPermissions(null, $endpointName)) { + Exceptions::error_rep("[API] Public access to '$endpointName' allowed – skipping authentication."); + return; + } + + $user = $_SERVER["PHP_AUTH_USER"] ?? null; + $pw = $_SERVER["PHP_AUTH_PW"] ?? null; + $this->__set("arbeitszeit", new Arbeitszeit()); $this->__set("benutzer", new Benutzer()); if (!$user) { @@ -40,34 +54,37 @@ public function __construct(){ } else { $this->__set("api_username", $user); } - + $this->__set("api_password", $pw or $this->authError($user)); - if(!$this->login(username: $user, password: $pw)){ + if (!$this->login(username: $user, password: $pw)) { $this->authError($user); } - $this->__set("eventHandler", new EventHandler()); } - public function __set($name, $value){ + public function __set($name, $value) + { $this->$name = $value; } - public function __get($name){ + public function __get($name) + { return $this->$name; } - public function authError($name = null){ + public function authError($name = null) + { Exceptions::error_rep("[API] Failed authentication for Toil API for user '{$name}'"); header("WWW-Authenticate: Basic realm='" . $this->bString . "'"); header("HTTP/1.0 401 Unauthorized"); die("Not authenticated"); } - public function login($username, $password){ - if($this->__get("benutzer")->get_user($username)){ + public function login($username, $password) + { + if ($this->__get("benutzer")->get_user($username)) { $data = $this->__get("benutzer")->get_user($username); - if(password_verify($password, $data["password"])){ + if (password_verify($password, $data["password"])) { Exceptions::error_rep("[API] Successfully authenticated user '$username' via API."); return true; } @@ -76,16 +93,17 @@ public function login($username, $password){ } } - public function routing($eventHandler){ + public function routing($eventHandler) + { Exceptions::error_rep("[API] Start API routing request and registering routes..."); $base = $this->__get("basepath"); $user = $_SERVER["PHP_AUTH_USER"]; - $eventHandler->register(EventHandler::EVENT_ADD_ROUTE, function(EventArgument $event) use ($base){ + $eventHandler->register(EventHandler::EVENT_ADD_ROUTE, function (EventArgument $event) use ($base) { $route = $event->route; - if(!$event->isSubRoute){ + if (!$event->isSubRoute) { return; } - switch(true){ + switch (true) { case $route instanceof ILoadableRoute: $route->prependUrl($base); break; @@ -100,7 +118,7 @@ public function routing($eventHandler){ $permissions = new Permissions; preg_match("/\/([^\/?]+)(\?.*)?$/m", $_SERVER["REQUEST_URI"], $matches); - if(!$permissions->checkPermissions($user, $matches[1])){ + if (!$permissions->checkPermissions($user, $matches[1])) { Exceptions::error_rep("[API] Failed checking permissions for expected endpoint: " . $matches[1]); header("Content-type: application/json"); header("HTTP/1.1 403 Forbidden"); @@ -109,86 +127,86 @@ public function routing($eventHandler){ } Router::addEventHandler($eventHandler); - Router::get("/api/v1/toil/getVersion", function(){ + Router::get("/api/v1/toil/getVersion", function () { Exceptions::error_rep("Accessing 'getVersion' endpoint. User: " . $this->__get("api_username")); Controller::createview("getVersion"); }); - Router::get("/api/v1/toil/healthcheck", function(){ + Router::get("/api/v1/toil/healthcheck", function () { Exceptions::error_rep("[API] User authenticated and accessing 'healthcheck' endpoint"); Controller::createview("healthcheck"); }); - Router::get("/api/v1/toil/getUserCount", function(){ + Router::get("/api/v1/toil/getUserCount", function () { Exceptions::error_rep("[API] User authenticated and acceessing 'getUserCount' endpoint"); Controller::createview("getUserCount"); }); - Router::get("/api/v1/toil/getApiVersion", function(){ + Router::get("/api/v1/toil/getApiVersion", function () { Exceptions::error_rep("[API] User authenticated and accessing 'getApiVersion' endpoint"); Controller::createview("getApiVersion"); }); - Router::get("/api/v1/toil/getLog", function(){ + Router::get("/api/v1/toil/getLog", function () { Exceptions::error_rep("[API] User authenticated and accessing 'getLog' endpoint"); Controller::createview("getLog"); }); - Router::get("/api/v1/toil/getWorktimes", function(){ + Router::get("/api/v1/toil/getWorktimes", function () { Exceptions::error_rep("[API] User authenticated and accessing 'getWorktimes' endpoint"); Controller::createview("getWorktimes"); }); - Router::get("/api/v1/toil/getVacations", function(){ + Router::get("/api/v1/toil/getVacations", function () { Exceptions::error_rep("[API] User authenticated and accessing 'getVacations' endpoint"); Controller::createview("getVacations"); }); - Router::get("/api/v1/toil/getUsers", function(){ + Router::get("/api/v1/toil/getUsers", function () { Exceptions::error_rep("[API] User authenticated and accessing 'getUsers' endpoint"); Controller::createview("getUsers"); }); - Router::get("/api/v1/toil/getUserDetails", function(){ + Router::get("/api/v1/toil/getUserDetails", function () { Exceptions::error_rep("[API] User authenticated and accessing 'getUserDetails' endpoint"); Controller::createview("getUserDetails"); }); - Router::get("/api/v1/toil/approveVacation", function(){ + Router::get("/api/v1/toil/approveVacation", function () { Exceptions::error_rep("[API] User authenticated and accessing 'approveVacation' endpoint"); Controller::createview("approveVacation"); }); - Router::get("/api/v1/toil/addWorktime", function(){ + Router::get("/api/v1/toil/addWorktime", function () { Exceptions::error_rep("[API] User authenticated and accessing 'addWorktime' endpoint"); Controller::createview("addWorktime"); }); - Router::get("/api/v1/toil/addVacation", function(){ + Router::get("/api/v1/toil/addVacation", function () { Exceptions::error_rep("[API] User authenticated and accessing 'addVacation' endpoint"); Controller::createview("addVacation"); }); - Router::get("/api/v1/toil/addProject", function(){ + Router::get("/api/v1/toil/addProject", function () { Exceptions::error_rep("[API] User authenticated and accessing 'addProject' endpoint"); Controller::createview("addProject"); }); - - Router::get("/api/v1/toil/getUserWorktimes", function(){ + + Router::get("/api/v1/toil/getUserWorktimes", function () { Exceptions::error_rep("[API] User authenticated and accessing 'getUserWorktimes' endpoint"); Controller::createview("getUserWorktimes"); }); - Router::get("/api/v1/toil/deleteWorktime", function(){ + Router::get("/api/v1/toil/deleteWorktime", function () { Exceptions::error_rep("[API] User authenticated and accessing 'deleteWorktime' endpoint"); Controller::createview("deleteWorktime"); }); - Router::get("/api/v1/toil/deleteUser", function(){ + Router::get("/api/v1/toil/deleteUser", function () { Exceptions::error_rep("[API] User authenticated and accessing 'deleteUser' endpoint"); Controller::createview("deleteUser"); }); - Router::get("/api/v1/toil/addUser", function(){ + Router::get("/api/v1/toil/addUser", function () { Exceptions::error_rep("[API] User authenticated and accessing 'addUser' endpoint"); Controller::createview("addUser"); }); - Router::get("/api/v1/toil/getOwnWorktime", function(){ + Router::get("/api/v1/toil/getOwnWorktime", function () { Exceptions::error_rep("[API] User authenticated and accessing 'getOwnWorktime' endpoint"); Controller::createview("getOwnWorktime"); }); @@ -196,8 +214,8 @@ public function routing($eventHandler){ // Loading all custom routes CustomRoutes::loadCustomRoutes(); - Router::error(function(Request $request, \Exception $e){ - switch($e->getCode()){ + Router::error(function (Request $request, \Exception $e) { + switch ($e->getCode()) { case 404: Exceptions::error_rep("[API] 404 Not found. Endpoint: " . $request->getUrl()); header("Content-type: application/json"); @@ -214,7 +232,8 @@ public function routing($eventHandler){ }); } - public function getResourceNameFromPath($path){ + public function getResourceNameFromPath($path) + { preg_match("/[^\/]+$/", $path, $match); return $match[0]; } diff --git a/api/v1/toil/VERSION b/api/v1/toil/VERSION index 1206807..b4ffd19 100644 --- a/api/v1/toil/VERSION +++ b/api/v1/toil/VERSION @@ -1 +1 @@ -v1.9 \ No newline at end of file +v1.11 \ No newline at end of file diff --git a/api/v1/toil/permissions.json b/api/v1/toil/permissions.json index ca00c3e..790c3e2 100644 --- a/api/v1/toil/permissions.json +++ b/api/v1/toil/permissions.json @@ -6,7 +6,7 @@ "approveVacation": 1, "deleteUser": 1, "deleteWorktime": 1, - "getApiVersion": 0, + "getApiVersion": 2, "getLog": 1, "getUserCount": 1, "getUserDetails": 1, @@ -16,7 +16,7 @@ "addOwnWorktime": 0, "addOwnVacation": 0, "getVacations": 1, - "getVersion": 0, + "getVersion": 2, "getWorktimes": 1, - "healthcheck": 0 + "healthcheck": 2 } \ No newline at end of file From 8e379fa1baa4421f519e5a3c413a23a8f54bb9c6 Mon Sep 17 00:00:00 2001 From: Ente Date: Sat, 29 Mar 2025 19:25:34 +0100 Subject: [PATCH 18/48] TT-7: Security Enhancements and fix sql --- api/v1/class/arbeitszeit.inc.php | 57 ++++++++ api/v1/class/benutzer/benutzer.arbeit.inc.php | 15 +++ api/v1/class/mode/mode.arbeit.inc.php | 8 ++ api/v1/class/nodes/json/nodes.json | 126 ++++++++++++++++++ api/v1/class/nodes/nodes.arbeit.inc.php | 66 +++++++++ .../notifications.arbeit.inc.php | 7 + api/v1/class/sickness/sickness.arbeit.inc.php | 12 ++ api/v1/class/vacation/vacation.arbeit.inc.php | 12 ++ api/v1/inc/arbeit.inc.php | 1 + setup/sql.sql | 4 + 10 files changed, 308 insertions(+) create mode 100644 api/v1/class/nodes/json/nodes.json create mode 100644 api/v1/class/nodes/nodes.arbeit.inc.php diff --git a/api/v1/class/arbeitszeit.inc.php b/api/v1/class/arbeitszeit.inc.php index c673d75..93646d3 100644 --- a/api/v1/class/arbeitszeit.inc.php +++ b/api/v1/class/arbeitszeit.inc.php @@ -12,6 +12,7 @@ use Arbeitszeit\Sickness; use Arbeitszeit\ExportModule; use Arbeitszeit\Mails; + use Arbeitszeit\Nodes; /** * Beinhaltet wesentliche Inhalte, wie Einstellungen, Arbeitszeiten erstellen, etc. * @@ -32,6 +33,7 @@ class Arbeitszeit private $vacation; private $exportModule; private $mails; + private $nodes; public function __construct() { @@ -65,6 +67,9 @@ public function init_lang() */ public function delete_worktime($id) { + if(!$this->nodes()->checkNode("arbeitszeit.inc", "delete_worktime")){ + return false; + } $data = $this->db->sendQuery("DELETE FROM arbeitszeiten WHERE id = ?")->execute([$id]); if ($data == false) { Exceptions::error_rep("An error occured while deleting an worktime entry. See previous message for more information."); @@ -83,6 +88,10 @@ public function delete_worktime($id) public static function add_easymode_worktime($username) { + $nodes = new Nodes; + if(!$nodes->checkNode("arbeitszeit.inc", "add_easymode_worktime")) { + return false; + } Exceptions::error_rep("Creating easymode worktime entry for user '{$username}'..."); $date = date("Y-m-d"); $time = date("H:i"); @@ -109,6 +118,10 @@ public static function add_easymode_worktime($username) public static function end_easymode_worktime($username, $id) { + $nodes = new Nodes; + if(!$nodes->checkNode("arbeitszeit.inc", "end_easymode_worktime")) { + return false; + } Exceptions::error_rep("Ending easymode worktime for user '{$username}'..."); $time = date("H:i"); $conn = new DB; @@ -133,6 +146,9 @@ public static function end_easymode_worktime($username, $id) public function start_easymode_pause_worktime($username, $id) { + if(!$this->nodes()->checkNode("arbeitszeit.inc", "start_easymode_pause_worktime")) { + return false; + } Exceptions::error_rep("Starting easymode pause for user '{$username}'..."); $time = date("H:i"); $user = new Benutzer; @@ -155,6 +171,9 @@ public function start_easymode_pause_worktime($username, $id) } public function end_easymode_pause_worktime($username, $id) { + if(!$this->nodes()->checkNode("arbeitszeit.inc", "end_easymode_pause_worktime")) { + return false; + } Exceptions::error_rep("Ending easymode pause for user '{$username}'..."); $time = date("H:i"); $user = new Benutzer; @@ -178,6 +197,9 @@ public function end_easymode_pause_worktime($username, $id) public function toggle_easymode($username) { + if(!$this->nodes()->checkNode("arbeitszeit.inc", "toggle_easymode")) { + return false; + } Exceptions::error_rep("Toggling easymode for user '{$username}'..."); $sql = "SELECT * FROM `users` WHERE username = ?;"; $res = $this->db->sendQuery($sql); @@ -210,6 +232,9 @@ public function toggle_easymode($username) public function get_easymode_status($username, $mode = 0) { + if(!$this->nodes()->checkNode("arbeitszeit.inc", "get_easymode_status")) { + return false; + } Exceptions::error_rep("Getting easymode status for user '{$username}'..."); $sql = "SELECT * FROM `users` WHERE username = ?;"; $res = $this->db->sendQuery($sql); @@ -246,6 +271,10 @@ public function get_easymode_status($username, $mode = 0) */ public static function check_easymode_worktime_finished($username) { + $nodes = new Nodes; + if(!$nodes->checkNode("arbeitszeit.inc", "check_easymode_worktime_finished")) { + return false; + } Exceptions::error_rep("Checking easymode worktime for user '{$username}'..."); $db = new DB; $sql = "SELECT * FROM `arbeitszeiten` WHERE active = '1' AND username = ?;"; @@ -280,6 +309,9 @@ public static function check_easymode_worktime_finished($username) */ public function add_worktime($start, $end, $location, $date, $username, $type, $pause = null, $meta = null) { + if(!$this->nodes()->checkNode("arbeitszeit.inc", "add_worktime")) { + return false; + } $user = new Benutzer; $usr = $user->get_user($username); if ($date > date("y-m-d") /*|| $date < date("y-m-d")*/) { @@ -356,6 +388,9 @@ private static function sanitizeOutput($data) public function get_all_worktime() { + if(!$this->nodes()->checkNode("arbeitszeit.inc", "get_all_worktime")) { + return false; + } Exceptions::error_rep("Getting all worktimes..."); $sql = "SELECT * FROM `arbeitszeiten`;"; $res = $this->db->sendQuery($sql); @@ -373,6 +408,9 @@ public function get_all_worktime() public function get_all_user_worktime($username) { + if(!$this->nodes()->checkNode("arbeitszeit.inc", "get_all_user_worktime")) { + return false; + } Exceptions::error_rep("Getting all worktimes for user '{$username}'..."); $sql = "SELECT * FROM `arbeitszeiten` WHERE username = ?;"; $res = $this->db->sendQuery($sql); @@ -390,6 +428,9 @@ public function get_all_user_worktime($username) public function get_specific_worktime_html(int $month, int $year) { + if(!$this->nodes()->checkNode("arbeitszeit.inc", "get_specific_worktime_html")) { + return false; + } Exceptions::error_rep("Getting worktimes rendered for month '{$month}' and year '{$year}'..."); $base_url = $ini = Arbeitszeit::get_app_ini()["general"]["base_url"]; $sql = "SELECT * FROM `arbeitszeiten` WHERE YEAR(schicht_tag) = ? AND MONTH(schicht_tag) = ? ORDER BY schicht_tag DESC;"; @@ -460,6 +501,9 @@ public function get_specific_worktime_html(int $month, int $year) } public function get_employee_worktime_html($username) { + if(!$this->nodes()->checkNode("arbeitszeit.inc", "get_employee_worktime_html")) { + return false; + } Exceptions::error_rep("Getting worktimes rendered for user '{$username}'..."); $sql = "SELECT * FROM `arbeitszeiten` WHERE username = ? ORDER BY id DESC;"; $res = $this->db->sendQuery($sql); @@ -519,6 +563,9 @@ public function get_employee_worktime_html($username) public function mark_for_review($id) { + if(!$this->nodes()->checkNode("arbeitszeit.inc", "mark_for_review")) { + return false; + } Exceptions::error_rep("Marking worktime with ID '{$id}' for review..."); $sql = "UPDATE `arbeitszeiten` SET `review` = '1' WHERE `id` = ?;"; $res = $this->db->sendQuery($sql)->execute([$id]); @@ -532,6 +579,9 @@ public function mark_for_review($id) public function unlock_for_review($id) { + if(!$this->nodes()->checkNode("arbeitszeit.inc", "unlock_for_review")) { + return false; + } Exceptions::error_rep("Unlocking worktime from review with ID '{$id}'..."); $sql = "UPDATE `arbeitszeiten` SET `review` = '0' WHERE `id` = ?;"; $res = $this->db->sendQuery($sql)->execute([$id]); @@ -781,6 +831,13 @@ public function mails(): Mails $this->mails = new Mails; return $this->mails; } + + public function nodes(): Nodes + { + if (!$this->nodes) + $this->nodes = new Nodes; + return $this->nodes; + } } } diff --git a/api/v1/class/benutzer/benutzer.arbeit.inc.php b/api/v1/class/benutzer/benutzer.arbeit.inc.php index 5f83fff..e76a9b6 100644 --- a/api/v1/class/benutzer/benutzer.arbeit.inc.php +++ b/api/v1/class/benutzer/benutzer.arbeit.inc.php @@ -21,6 +21,9 @@ public function __construct() */ public function create_user($username, $name, $email, $password, $isAdmin = 0) { + if($this->nodes()->checkNode("benutzer.inc", "create_user") == false){ + return false; + } #$originalFunction = function($username, $name, $email, $password, $isAdmin){ Exceptions::error_rep("Creating user '$username'..."); $password = password_hash($password, PASSWORD_DEFAULT); @@ -52,6 +55,9 @@ public function create_user($username, $name, $email, $password, $isAdmin = 0) */ public function delete_user($id) { + if($this->nodes()->checkNode("benutzer.inc", "delete_user") == false){ + return false; + } Exceptions::error_rep("Deleting user with id '$id'..."); $sql = "DELETE FROM `users` WHERE id = ?;"; $data = $this->db->sendQuery($sql)->execute([$id]); @@ -188,6 +194,9 @@ public function get_all_users() */ public function get_all_users_html() { + if($this->nodes()->checkNode("benutzer.inc", "get_all_users_html") == false){ + return false; + } Exceptions::error_rep("Getting all users..."); $base_url = $ini = $this->get_app_ini()["general"]["base_url"]; $sql = "SELECT * FROM `users`;"; @@ -232,6 +241,9 @@ public function get_all_users_html() */ public function get_user_html($username) { + if($this->nodes()->checkNode("benutzer.inc", "get_user_html") == false){ + return false; + } Exceptions::error_rep("Getting user '$username'..."); $base_url = $ini = $this->get_app_ini()["general"]["base_url"]; $data = $this->get_user($username); @@ -281,6 +293,9 @@ public function is_admin($user) public function editUserProperties(mixed $username_or_id, string $name, mixed $value): bool { + if($this->nodes()->checkNode("benutzer.inc", "editUserProperties") == false){ + return false; + } if ( !isset($_SESSION["username"]) || !$this->is_admin($this->get_user($_SESSION["username"])) diff --git a/api/v1/class/mode/mode.arbeit.inc.php b/api/v1/class/mode/mode.arbeit.inc.php index 9c11000..44d9bed 100644 --- a/api/v1/class/mode/mode.arbeit.inc.php +++ b/api/v1/class/mode/mode.arbeit.inc.php @@ -18,6 +18,10 @@ public static function check($username){ private static function get_normal_mode_html(){ + $nodes = new Nodes; + if($nodes->checkNode("mode.inc", "get_normal_mode_html") == false){ + return; + } $i18n = new i18n; $loc = $i18n->loadLanguage(null, "mode/easymode"); $data = <<< DATA @@ -60,6 +64,10 @@ private static function get_normal_mode_html(){ } private static function get_easymode_html(){ + $nodes = new Nodes; + if($nodes->checkNode("mode.inc", "get_easymode_html") == false){ + return; + } $i18n = new i18n; $loc = $i18n->loadLanguage(null, "mode/easymode"); $active = Arbeitszeit::check_easymode_worktime_finished($_SESSION["username"]); diff --git a/api/v1/class/nodes/json/nodes.json b/api/v1/class/nodes/json/nodes.json new file mode 100644 index 0000000..d0a5b09 --- /dev/null +++ b/api/v1/class/nodes/json/nodes.json @@ -0,0 +1,126 @@ +{ + "nodes": { + "functions": { + "notifications.inc": { + "create_notifications_entry": true, + "create_notifications_entry_P": 1, + + "notifications_delete": true, + "notifications_delete_P": 1, + + "get_notifications_edit_html": true, + "get_notifications_edit_html_P": 1, + + "get_all_notifications": true, + "get_all_notifications_P": 0, + + "get_notifications_html": true, + "get_notifications_html_P": 0, + + "get_notifications_entry": true, + "get_notifications_entry_P": 0, + + "edit_notifications_entry": true, + "edit_notifications_entry_P": 1, + + "delete_notifications_entry": true, + "delete_notifications_entry_P": 1 + }, + "benutzer.inc": { + "editUserProperties": true, + "editUserProperties_P": 1, + + "get_user_html": true, + "get_user_html_P": 0, + + "get_all_users_html": true, + "get_all_users_html_P": 1, + + "delete_user": true, + "delete_user_P": 1, + + "create_user": true, + "create_user_P": 1 + }, + "mode.inc": { + "get_normal_mode_html": true, + "get_normal_mode_html_P": 0, + + "get_easymode_html": true, + "get_easymode_html_P": 0 + }, + "sickness.inc": { + "add_sickness": true, + "add_sickness_P": 0, + + "remove_sickness": true, + "remove_sickness_P": 1, + + "change_status": true, + "change_status_P": 1, + + "display_sickness_all": true, + "display_sickness_all_P": 1 + }, + "vacation.inc": { + "add_vacation": true, + "add_vacation_P": 0, + + "remove_vacation": true, + "remove_vacation_P": 1, + + "change_status": true, + "change_status_P": 1, + + "display_vacation_all": true, + "display_vacation_all_P": 1 + }, + "arbeitszeit.inc": { + "delete_worktime": true, + "delete_worktime_P": 1, + + "add_easymode_worktime": true, + "add_easymode_worktime_P": 0, + + "end_easymode_worktime": true, + "end_easymode_worktime_P": 0, + + "start_easymode_pause_worktime": true, + "start_easymode_pause_worktime_P": 0, + + "end_easymode_pause_worktime": true, + "end_easymode_pause_worktime_P": 0, + + "toggle_easymode": true, + "toggle_easymode_P": 0, + + "get_easymode_status": true, + "get_easymode_status_P": 0, + + "check_easymode_worktime_finished": true, + "check_easymode_worktime_finished_P": 0, + + "add_worktime": true, + "add_worktime_P": 0, + + "get_all_worktime": true, + "get_all_worktime_P": 1, + + "get_all_user_worktime": true, + "get_all_user_worktime_P": 0, + + "get_specific_worktime_html": true, + "get_specific_worktime_html_P": 1, + + "get_employee_worktime_html": true, + "get_employee_worktime_html_P": 0, + + "mark_for_review": true, + "mark_for_review_P": 1, + + "unlock_for_review": true, + "unlock_for_review_P": 1 + } + } + } +} diff --git a/api/v1/class/nodes/nodes.arbeit.inc.php b/api/v1/class/nodes/nodes.arbeit.inc.php new file mode 100644 index 0000000..b16db4c --- /dev/null +++ b/api/v1/class/nodes/nodes.arbeit.inc.php @@ -0,0 +1,66 @@ +benutzer()->is_admin($this->benutzer()->get_user($_SESSION["username"])) ? 1 : 0; + $nodeSet = $panel + ? $data["nodes"]["panels"][$file] ?? null + : $data["nodes"]["functions"][$file] ?? null; + + if (!$nodeSet || !is_array($nodeSet)) { + Exceptions::error_rep("checkNode: Invalid node set for $file, expected array, got " . gettype($nodeSet)); + return false; + } + + $baseKey = $function; + $permKey = $function . '_P'; + + $allowed = $nodeSet[$baseKey] ?? false; + + if ($allowed === false) { + Exceptions::error_rep("checkNode: Access denied for function $function in file $file"); + return false; + } + + if (!array_key_exists($permKey, $nodeSet)) { + Exceptions::error_rep("checkNode: Permission key $permKey not found in node set for $file"); + return $allowed === true; + } + + $level = $nodeSet[$permKey]; + if (!in_array($level, [0, 1, 2], true)) { + Exceptions::error_rep("checkNode: Invalid permission level for $function"); + return false; + } + if ($level === 2) { + //access granted for public + Exceptions::error_rep("checkNode: Access granted for function $function in file $file, user level: $userIsAdmin, required level: $level"); + return true; + } + if ($level === 1 && $userIsAdmin === 1) { + //access granted for admin user + Exceptions::error_rep("checkNode: Access granted for function $function in file $file, user level: $userIsAdmin, required level: $level"); + return true; + } + if ($level === 0 && ($userIsAdmin === 0 || $userIsAdmin === 1)) { + //acces granted for normal user + Exceptions::error_rep("checkNode: Access granted for function $function in file $file, user level: $userIsAdmin, required level: $level"); + return true; + } + Exceptions::error_rep("checkNode: Access denied for function $function in file $file, user level: $userIsAdmin, required level: $level"); + return false; + } +} \ No newline at end of file diff --git a/api/v1/class/notifications/notifications.arbeit.inc.php b/api/v1/class/notifications/notifications.arbeit.inc.php index 96bfc51..7e5a39f 100644 --- a/api/v1/class/notifications/notifications.arbeit.inc.php +++ b/api/v1/class/notifications/notifications.arbeit.inc.php @@ -40,6 +40,7 @@ public function notifications_delete(){ } public function get_notifications_edit_html(){ + if(!$this->nodes()->checkNode("notifications.inc", "get_notifications_edit_html")) return "

{$this->i18n["no_entries"]}

"; Exceptions::error_rep("[NOTIFICATIONS] Getting notifications edit html..."); $sql = "SELECT * FROM `kalender` ORDER BY id DESC;"; $res = $this->db->sendQuery($sql); @@ -70,6 +71,7 @@ public function get_notifications_edit_html(){ } public function get_all_notifications(){ + if(!$this->nodes()->checkNode("notifications.inc", "get_all_notifications")) return null; Exceptions::error_rep("[NOTIFICATIONS] Getting all notifications..."); $sql = "SELECT * FROM `kalender` ORDER BY id DESC;"; $res = $this->db->sendQuery($sql); @@ -99,6 +101,7 @@ public function get_all_notifications(){ } public function get_notifications_html(){ + if(!$this->nodes()->checkNode("notifications.inc", "get_notifications_html")) return null; Exceptions::error_rep("[NOTIFICATIONS] Getting notifications html..."); $base_url = Arbeitszeit::get_app_ini()["general"]["base_url"]; $sql = "SELECT * FROM `kalender` ORDER BY id DESC;"; @@ -137,6 +140,7 @@ public function get_notifications_html(){ * */ public function get_notifications_entry($id){ + if(!$this->nodes()->checkNode("notifications.inc", "get_notifications_entry")) return null; Exceptions::error_rep("[NOTIFICATIONS] Getting notifications entry with ID '$id'..."); $sql = "SELECT * FROM `kalender` WHERE id = ?"; $res = $this->db->sendQuery($sql); @@ -171,6 +175,7 @@ public function get_notifications_entry($id){ * @param bool|array Returns "true" on success and an error array on failure */ public function create_notifications_entry($time, $date, $location, $comment){ + if(!$this->nodes()->checkNode("notifications.inc", "create_notifications_entry")) return false; Exceptions::error_rep("[NOTIFICATIONS] Creating new notifications entry..."); $sql = "INSERT INTO `kalender` (`datum`, `uhrzeit`, `ort`, `notiz`) VALUES (?, ?, ?, ?)"; $res = $this->db->sendQuery($sql)->execute([$date, $time, $location, $comment]); @@ -199,6 +204,7 @@ public function create_notifications_entry($time, $date, $location, $comment){ * @Note All parameters are required, if not set, the function will return an error array */ public function edit_notifications_entry($id, $time, $date, $location, $comment){ + if(!$this->nodes()->checkNode("notifications.inc", "edit_notifications_entry")) return false; Exceptions::error_rep("[NOTIFICATIONS] Editing notifications entry with ID '$id'..."); $sql = "UPDATE `kalender` SET `datum` = ?, `uhrzeit` = ?, `ort` = ?, `notiz` = ? WHERE id = ?;"; $res = $this->db->sendQuery($sql)->execute([$date, $time, $location, $comment, $id]); @@ -223,6 +229,7 @@ public function edit_notifications_entry($id, $time, $date, $location, $comment) * @return bool|array Returns "true" on success and an error array on failure */ public function delete_notifications_entry($id){ + if(!$this->nodes()->checkNode("notifications.inc", "delete_notifications_entry")) return false; Exceptions::error_rep("[NOTIFICATIONS] Deleting notifications entry with ID '$id'..."); $sql = "DELETE FROM `kalender` WHERE id = ?;"; $res = $this->db->sendQuery($sql)->execute([$id]); diff --git a/api/v1/class/sickness/sickness.arbeit.inc.php b/api/v1/class/sickness/sickness.arbeit.inc.php index 440d659..9ec6c8c 100644 --- a/api/v1/class/sickness/sickness.arbeit.inc.php +++ b/api/v1/class/sickness/sickness.arbeit.inc.php @@ -11,6 +11,9 @@ class Sickness extends Arbeitszeit public function add_sickness($start, $stop, $user = null) { + if($this->nodes()->checkNode("sickness.inc", "add_sickness") == false){ + return false; + } Exceptions::error_rep("[SICK] Adding sickness for user '{$user}'..."); $user = $_SESSION["username"]; $dateString = $start; @@ -34,6 +37,9 @@ public function add_sickness($start, $stop, $user = null) } public function remove_sickness($id){ # admin function only + if($this->nodes()->checkNode("sickness.inc", "remove_sickness") == false){ + return false; + } Exceptions::error_rep("[SICK] Removing sickness with id '{$id}'..."); $data = $this->db()->sendQuery("DELETE * FROM sick WHERE id = ?")->execute([$id]); if($data == false){ @@ -45,6 +51,9 @@ public function remove_sickness($id){ # admin function only public function change_status($id, $new_state = 3) # admin function only { + if($this->nodes()->checkNode("sickness.inc", "change_status") == false){ + return false; + } Exceptions::error_rep("[SICK] Changing status for sickness id '{$id}' to '{$new_state}'..."); if($new_state == 1 /* approve */){ $sql = "UPDATE `sick` SET `status` = 'approved' WHERE `id` = ?;"; @@ -65,6 +74,9 @@ public function change_status($id, $new_state = 3) # admin function only } public function display_sickness_all(){ # admin function only + if($this->nodes()->checkNode("sickness.inc", "display_sickness_all") == false){ + return false; + } Exceptions::error_rep("[SICK] Displaying all sicknesses..."); $i18n = $this->i18n()->loadLanguage(null, "worktime/sick/all", "admin"); diff --git a/api/v1/class/vacation/vacation.arbeit.inc.php b/api/v1/class/vacation/vacation.arbeit.inc.php index b301231..5400fec 100644 --- a/api/v1/class/vacation/vacation.arbeit.inc.php +++ b/api/v1/class/vacation/vacation.arbeit.inc.php @@ -17,6 +17,9 @@ public function __construct(){ public function add_vacation($start, $stop, $username = null) { + if($this->nodes()->checkNode("vacation.inc", "add_vacation") == false){ + return false; + } Exceptions::error_rep("[VACATION] Adding vacation for user '{$username}'..."); if($username != null){ $user = $_SESSION["username"]; @@ -42,6 +45,9 @@ public function add_vacation($start, $stop, $username = null) } public function remove_vacation($id){ # admin function only + if($this->nodes()->checkNode("vacation.inc", "remove_vacation") == false){ + return false; + } Exceptions::error_rep("[VACATION] Removing vacation with id '{$id}'..."); $sql = "DELETE * FROM `vacation` WHERE id = ?"; $data = $this->db->sendQuery($sql)->execute(array([$id])); @@ -55,6 +61,9 @@ public function remove_vacation($id){ # admin function only public function change_status($id, $new_state = 3) # admin function only { + if($this->nodes()->checkNode("vacation.inc", "change_status") == false){ + return false; + } Exceptions::error_rep("[VACATION] Changing status for vacation id '{$id}' to '{$new_state}'..."); if($new_state == 1 /* approve */){ $sql = "UPDATE `vacation` SET `status` = 'approved' WHERE `id` = ?;"; @@ -93,6 +102,9 @@ public function get_vacation($id, $mode = 1){ } public function display_vacation_all(){ # admin function only + if($this->nodes()->checkNode("vacation.inc", "display_vacation_all") == false){ + return false; + } Exceptions::error_rep("[VACATION] Displaying all vacations..."); $i18n = $this->i18n()->loadLanguage(null, "worktime/vacation/all", "admin"); diff --git a/api/v1/inc/arbeit.inc.php b/api/v1/inc/arbeit.inc.php index 8723275..2ef5814 100644 --- a/api/v1/inc/arbeit.inc.php +++ b/api/v1/inc/arbeit.inc.php @@ -9,6 +9,7 @@ } require_once dirname(__DIR__, 1) . "/class/arbeitszeit.inc.php"; +require_once dirname(__DIR__, 1) . "/class/nodes/nodes.arbeit.inc.php"; require_once dirname(__DIR__, 1) . "/class/db/db.arbeit.inc.php"; require_once dirname(__DIR__, 1) . "/class/notifications/notifications.arbeit.inc.php"; require_once dirname(__DIR__, 1) . "/class/i18n/i18n.arbeit.inc.php"; diff --git a/setup/sql.sql b/setup/sql.sql index 53e9549..be96fd1 100644 --- a/setup/sql.sql +++ b/setup/sql.sql @@ -67,6 +67,10 @@ CREATE TABLE `vacation` ( `status` text NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; +CREATE TABLE `scheme` ( + `v` INT(11) NULL DEFAULT '1' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + ALTER TABLE `arbeitszeiten` ADD PRIMARY KEY (`id`); From d502fa62210c0a049bb88fc78700633e8b50801e Mon Sep 17 00:00:00 2001 From: Ente Date: Sun, 30 Mar 2025 15:44:23 +0200 Subject: [PATCH 19/48] TT-4: Implement NFC token login --- .../class/plugins/plugins/nfclogin/README.md | 26 ++++++ .../class/plugins/plugins/nfclogin/plugin.yml | 15 +++ .../plugins/plugins/nfclogin/src/Main.php | 92 +++++++++++++++++++ .../plugins/nfclogin/src/read_nfc_uid.py | 19 ++++ .../src/routes/readNfc.ep.toil.arbeit.inc.php | 56 +++++++++++ .../plugins/plugins/nfclogin/views/cards.php | 68 ++++++++++++++ 6 files changed, 276 insertions(+) create mode 100644 api/v1/class/plugins/plugins/nfclogin/README.md create mode 100644 api/v1/class/plugins/plugins/nfclogin/plugin.yml create mode 100644 api/v1/class/plugins/plugins/nfclogin/src/Main.php create mode 100644 api/v1/class/plugins/plugins/nfclogin/src/read_nfc_uid.py create mode 100644 api/v1/class/plugins/plugins/nfclogin/src/routes/readNfc.ep.toil.arbeit.inc.php create mode 100644 api/v1/class/plugins/plugins/nfclogin/views/cards.php diff --git a/api/v1/class/plugins/plugins/nfclogin/README.md b/api/v1/class/plugins/plugins/nfclogin/README.md new file mode 100644 index 0000000..e3a4d0c --- /dev/null +++ b/api/v1/class/plugins/plugins/nfclogin/README.md @@ -0,0 +1,26 @@ +# NFClogin plugin + +This pluin allows you to interact with a PC/SC device (e.g. NFC-card reader/writer) to login to the system. + +## Requirements + +Please make sure your PHP instance does not block `exec()` function. +This plugin requires the `pcscd` service to be running on the system. You can install it using the following command: + +```bash +sudo apt update +sudo apt install pcscd pcsc-tools libpcsclite-dev python3-pip +pip3 install pyscard +sudo systemctl enable --now pcscd + +``` + +For Windows: + +- Download Python3 +- Download Visual C++ Redistributable +- Install official drivers for your NFC reader +- Install `pyscard` using pip: +```bash +pip install pyscard +``` diff --git a/api/v1/class/plugins/plugins/nfclogin/plugin.yml b/api/v1/class/plugins/plugins/nfclogin/plugin.yml new file mode 100644 index 0000000..1b0f4a3 --- /dev/null +++ b/api/v1/class/plugins/plugins/nfclogin/plugin.yml @@ -0,0 +1,15 @@ +name: nfclogin +src: /src +main: Main +namespace: NFClogin +author: Ente +description: 'Allow the use of NFC cards to login.' +version: '1.0' +api: 0.1 +permissions: none +enabled: true +custom.values: + license: LIC +nav_links: + 'Manage Cards': views/cards.php + 'Manage Card Settings': views/admin.php \ No newline at end of file diff --git a/api/v1/class/plugins/plugins/nfclogin/src/Main.php b/api/v1/class/plugins/plugins/nfclogin/src/Main.php new file mode 100644 index 0000000..1ef7e14 --- /dev/null +++ b/api/v1/class/plugins/plugins/nfclogin/src/Main.php @@ -0,0 +1,92 @@ + null, + "username" => null, + "pin" => null, + ]; + private string $mastertoken; + public $setup; + public $code; + + public function __construct() { + $this->set_plugin_configuration(); + $this->set_log_append(); + + $this->setup(); + } + + public function setup(): void { + $this->register_routes(); + } + + public function register_routes(): void { + Exceptions::error_rep("{$this->log_append} Registering custom routes..."); + CustomRoutes::registerCustomRoute("readNfc", "/api/v1/class/plugins/plugins/nfclogin/src/routes/readNfc.ep.toil.arbeit.inc.php", 1); + } + + public function set_log_append(): void { + $v = $this->read_plugin_configuration("nfclogin")["version"] ?? "unknown"; + $this->log_append = "[nfclogin v{$v}]"; + } + + public function get_log_append(): string { + return $this->log_append; + } + + public function set_plugin_configuration(): void { + $this->plugin_configuration = $this->read_plugin_configuration("nfclogin"); + } + + public function get_plugin_configuration(): array { + return $this->plugin_configuration; + } + + public function onDisable(): void { + $this->log_append = $this->get_log_append(); + } + + public function onEnable(): void {} + + public function onLoad(): void {} + + public function readCard(): ?array { + $scriptPath = __DIR__ . '/read_nfc_uid.py'; + exec("python3 " . escapeshellarg($scriptPath) . " 2>&1", $output, $status); + + if ($status !== 0) { + return [ + "error" => "Helper script execution failed", + "output" => $output + ]; + } + + $json = implode("", $output); + $data = json_decode($json, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + return [ + "error" => "Invalid JSON response from helper script", + "raw_output" => $output + ]; + } + + return $data; + } +} diff --git a/api/v1/class/plugins/plugins/nfclogin/src/read_nfc_uid.py b/api/v1/class/plugins/plugins/nfclogin/src/read_nfc_uid.py new file mode 100644 index 0000000..96bde5f --- /dev/null +++ b/api/v1/class/plugins/plugins/nfclogin/src/read_nfc_uid.py @@ -0,0 +1,19 @@ +from smartcard.System import readers +from smartcard.util import toHexString + +r = readers() +if not r: + print("No reader found") + exit(1) + +reader = r[0] +connection = reader.createConnection() +connection.connect() + +GET_UID = [0xFF, 0xCA, 0x00, 0x00, 0x00] # Command to get UID +data, sw1, sw2 = connection.transmit(GET_UID) + +if sw1 == 0x90 and sw2 == 0x00: + print('{"uid": "' + ''.join(format(x, '02X') for x in data) + '"}') +else: + print('{"error": "Failed to get UID"}') diff --git a/api/v1/class/plugins/plugins/nfclogin/src/routes/readNfc.ep.toil.arbeit.inc.php b/api/v1/class/plugins/plugins/nfclogin/src/routes/readNfc.ep.toil.arbeit.inc.php new file mode 100644 index 0000000..4a6eb96 --- /dev/null +++ b/api/v1/class/plugins/plugins/nfclogin/src/routes/readNfc.ep.toil.arbeit.inc.php @@ -0,0 +1,56 @@ +$name = $value; + } + + public function __get($name) + { + return $this->$name; + } + + public function get() + { + header('Content-Type: application/json'); + + try { + $nfc = new NFClogin; + $data = $nfc->readCard(); + echo json_encode($data); + } catch (\Exception $e) { + echo json_encode(["error" => true, "message" => $e->getMessage()]); + } + } + + public function post($post = null) + { + // Optional + } + + public function delete() + { + // Optional + } + + public function put() + { + // Optional + } + } +} diff --git a/api/v1/class/plugins/plugins/nfclogin/views/cards.php b/api/v1/class/plugins/plugins/nfclogin/views/cards.php new file mode 100644 index 0000000..2bdded3 --- /dev/null +++ b/api/v1/class/plugins/plugins/nfclogin/views/cards.php @@ -0,0 +1,68 @@ + +
+

NFCLogin Plugin

+

Please reload the page while holding the NFC card to read it.

+ + + + + + +
+

+ readCard(); + + if ($cardData) { + echo "Output/UUID

";
+            echo htmlspecialchars(json_encode($cardData, JSON_PRETTY_PRINT));
+            echo "
"; + } else { + echo "No card detected."; + } + } catch (Exception $e) { + echo "Error: " . $e->getMessage(); + } + ?> +

+
From 8d44b62833f7ea978bad0bce203717949712132e Mon Sep 17 00:00:00 2001 From: Ente Date: Sun, 30 Mar 2025 16:26:05 +0200 Subject: [PATCH 20/48] TT-4: Fix missing class --- .../plugins/nfclogin/src/routes/readNfc.ep.toil.arbeit.inc.php | 1 + 1 file changed, 1 insertion(+) diff --git a/api/v1/class/plugins/plugins/nfclogin/src/routes/readNfc.ep.toil.arbeit.inc.php b/api/v1/class/plugins/plugins/nfclogin/src/routes/readNfc.ep.toil.arbeit.inc.php index 4a6eb96..3e3303a 100644 --- a/api/v1/class/plugins/plugins/nfclogin/src/routes/readNfc.ep.toil.arbeit.inc.php +++ b/api/v1/class/plugins/plugins/nfclogin/src/routes/readNfc.ep.toil.arbeit.inc.php @@ -2,6 +2,7 @@ namespace Toil { require_once $_SERVER["DOCUMENT_ROOT"] . "/api/v1/inc/arbeit.inc.php"; require_once $_SERVER["DOCUMENT_ROOT"] . "/vendor/autoload.php"; + require_once $_SERVER["DOCUMENT_ROOT"] . "/api/v1/class/plugins/loader.plugins.arbeit.inc.php"; require_once dirname(__DIR__, 2) . "/src/Main.php"; use Toil\EP; From 23c5ceb2a36ea58df31fb25375c5addd627cb0f1 Mon Sep 17 00:00:00 2001 From: Ente Date: Sun, 30 Mar 2025 16:30:06 +0200 Subject: [PATCH 21/48] TT-4: Fix wrong class name --- .../plugins/nfclogin/src/routes/readNfc.ep.toil.arbeit.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/v1/class/plugins/plugins/nfclogin/src/routes/readNfc.ep.toil.arbeit.inc.php b/api/v1/class/plugins/plugins/nfclogin/src/routes/readNfc.ep.toil.arbeit.inc.php index 3e3303a..fbdd722 100644 --- a/api/v1/class/plugins/plugins/nfclogin/src/routes/readNfc.ep.toil.arbeit.inc.php +++ b/api/v1/class/plugins/plugins/nfclogin/src/routes/readNfc.ep.toil.arbeit.inc.php @@ -9,7 +9,7 @@ use Arbeitszeit\Arbeitszeit; use NFClogin\NFClogin; - class getWorktimes implements EPInterface + class readNfc implements EPInterface { public function __construct() { From 82a0ba3b6b2ce8ade2e5621958342953bf7489b5 Mon Sep 17 00:00:00 2001 From: Ente Date: Mon, 31 Mar 2025 18:54:11 +0200 Subject: [PATCH 22/48] TT-6: Rewrite db update process --- README.md | 12 +- api/v1/class/arbeitszeit.inc.php | 4 +- api/v1/class/updates/data/migrations/2.sql | 18 --- api/v1/class/updates/data/scheme.db | 1 - api/v1/class/updates/updates.arbeit.inc.php | 91 --------------- api/v1/inc/arbeit.inc.php | 7 +- composer.json | 3 +- .../20250331155531_init_az_scheme.php | 36 ++++++ .../20250331155756_init_notif_scheme.php | 27 +++++ .../20250331160130_init_sick_scheme.php | 28 +++++ .../20250331160400_init_vacation_scheme.php | 28 +++++ .../20250331160543_init_user_scheme.php | 29 +++++ .../20250331162111_init_schicht_scheme.php | 27 +++++ .../20250331164242_init_first_user.php | 37 ++++++ phinx.php | 27 +++++ setup/sql.sql | 110 ------------------ setup/upgrade.php | 16 --- setup/usercreate.php | 2 + 18 files changed, 250 insertions(+), 253 deletions(-) delete mode 100644 api/v1/class/updates/data/migrations/2.sql delete mode 100644 api/v1/class/updates/data/scheme.db delete mode 100644 api/v1/class/updates/updates.arbeit.inc.php create mode 100644 migrations/migrations/20250331155531_init_az_scheme.php create mode 100644 migrations/migrations/20250331155756_init_notif_scheme.php create mode 100644 migrations/migrations/20250331160130_init_sick_scheme.php create mode 100644 migrations/migrations/20250331160400_init_vacation_scheme.php create mode 100644 migrations/migrations/20250331160543_init_user_scheme.php create mode 100644 migrations/migrations/20250331162111_init_schicht_scheme.php create mode 100644 migrations/migrations/20250331164242_init_first_user.php create mode 100644 phinx.php delete mode 100644 setup/sql.sql delete mode 100644 setup/upgrade.php diff --git a/README.md b/README.md index d1e8e66..a027af7 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Additional functionality can be unlocked with TimeTrack Oval ### Requirements - at least PHP 8.2 (`curl|gd|gmp|intl|mbstring|mysqli|openssl|xsl|gettext|dom|ldap`) -- composer (to install dependencies; phpmailer: for sending emails via smtp, parsedown: markdown parser for the `CHANGELOG.md`, simple-router: does the API routing, yaml: for reading plugin yaml files, ldaptools: for LDAP authentication, dompdf: for PDF generation) +- composer (to install dependencies; phpmailer: for sending emails via smtp, parsedown: markdown parser for the `CHANGELOG.md`, simple-router: does the API routing, yaml: for reading plugin yaml files, ldaptools: for LDAP authentication, dompdf: for PDF generation, phinx: for database migrations) - Apache2.4 with enabled rewrite mod (optional) This software has been tested on Debian 11/12, XAMPP, PHP internal server (e.g. `php -S 0.0.0.0:80`). @@ -36,11 +36,10 @@ Simply install the software by following these steps: - Git clone timetrack to e.g. `/var/www`: `cd /var/www && git clone https://github.com/Ente/timetrack.git && cd timetrack` - Install requirements for composer `composer install` - Create a new database, e.g. with the name `ab` and create a dedicated user, login (`mysql -u root -p`) then e.g. `timetool`: `CREATE DATABASE ab;` and `CREATE USER 'timetool'@'localhost' IDENTIFIED BY 'yourpassword';` and `GRANT ALL PRIVILEGES ON ab.* TO 'timetool'@'localhost';` don't forget to `FLUSH PRIVILEGES;`! -- Import the `setup/sql.sql` into your database, e.g. `mysql -u timetool -p ab < /full/path/to/sql.sql` -- To create your first user, run the `setup/usercreate.php` file, e.g. `php ./usercreate.php admin yourpassword email@admin.com` - `usercreate.php [USERNAME] [PASSWORD] [EMAIL]` -- Run the statement printed by the `usercreate.php` inside your database (`mysql -u root -p` and `use ab;` then the statement). - Configure `app.json` (see below - required changes: `base_url`, `db_user`, `db_password`, `smtp` section and any other if your installation is different) then `mv api/v1/inc/app.json.sample app.json && cd /var/www/timetrack` +- Run DB migrations: `vendor/bin/phinx migrate` - Start webserver e.g. `service apache2 stop && php -S 0.0.0.0:80` or using apache2 (then you have to configure the `sites-available` conf yourself) +- You can then access TimeTrack in your browser at `http://localhost`, default login is `admin` with password `admin`. Create yourself a new admin account, login and delete the default account afterwards. ### Configure app.json @@ -187,7 +186,4 @@ If downloaded any other way, just make sure to copy and paste the new files into ### Database -You can update the database by downloading the `setup/upgrade.php` file into your local `setup` directory. -From here on just edit the `$missingUpdate` variable to the desired version as specified. - -Please be aware that you are not able to skip an database update. You have to update one by one, e.g. from 1 -> 2, 2 -> 3, ... +You can update the database by using `vendor/bin/phinx migrate` to migrate to latest release or `vendor/bin/phinx rollback` to rollback. \ No newline at end of file diff --git a/api/v1/class/arbeitszeit.inc.php b/api/v1/class/arbeitszeit.inc.php index 93646d3..350dbcc 100644 --- a/api/v1/class/arbeitszeit.inc.php +++ b/api/v1/class/arbeitszeit.inc.php @@ -343,8 +343,8 @@ public function add_worktime($start, $end, $location, $date, $username, $type, $ */ public static function get_app_ini() { - $ini_path = $_SERVER["DOCUMENT_ROOT"] . "/api/v1/inc/app.ini"; - $json_path = $_SERVER["DOCUMENT_ROOT"] . "/api/v1/inc/app.json"; + $ini_path = dirname(__DIR__, 3) . "/api/v1/inc/app.ini"; + $json_path = dirname(__DIR__, 3) . "/api/v1/inc/app.json"; Exceptions::error_rep("Loading application configuration..."); diff --git a/api/v1/class/updates/data/migrations/2.sql b/api/v1/class/updates/data/migrations/2.sql deleted file mode 100644 index df573db..0000000 --- a/api/v1/class/updates/data/migrations/2.sql +++ /dev/null @@ -1,18 +0,0 @@ -ALTER TABLE arbeitszeiten -ADD COLUMN project VARCHAR(255); - -CREATE TABLE `projects` ( - `id` int(11) NOT NULL, - `name` text NOT NULL, - `users` text NOT NULL, - `description` text NOT NULL, - `note` text NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_german1_ci; - -ALTER TABLE projects - ADD PRIMARY KEY (id) -ALTER TABLE projects - MODIFY id int(11) NOT NULL AUTO_INCREMENT; - -UPDATE scheme -SET v = '2'; diff --git a/api/v1/class/updates/data/scheme.db b/api/v1/class/updates/data/scheme.db deleted file mode 100644 index 56a6051..0000000 --- a/api/v1/class/updates/data/scheme.db +++ /dev/null @@ -1 +0,0 @@ -1 \ No newline at end of file diff --git a/api/v1/class/updates/updates.arbeit.inc.php b/api/v1/class/updates/updates.arbeit.inc.php deleted file mode 100644 index 8d3eb76..0000000 --- a/api/v1/class/updates/updates.arbeit.inc.php +++ /dev/null @@ -1,91 +0,0 @@ -$name; - } - - public function __set($name, $value){ - $this->$name = $value; - } - - private function request_schemev(){ - $conn = new DB; - Exceptions::error_rep("Checking database scheme version..."); - try { - $sql = "SELECT * FROM scheme;"; - $res = $conn->sendQuery($sql); - $res->execute(); - Exceptions::error_rep("Successfully checked database scheme version."); - $data = $res->fetch(\PDO::FETCH_ASSOC)["v"]; - } catch(\Exception $e){ - Exceptions::error_rep("Database scheme not found. Creating new scheme..."); - $sql = "CREATE TABLE scheme (v INT DEFAULT 1);"; - $sql1 = "INSERT INTO scheme (v) VALUES (1);"; - $conn->simpleQuery($sql); - $conn->simpleQuery($sql1); - Exceptions::error_rep("Successfully created new database scheme. Retrying to get scheme version..."); - $this->request_schemev(); - } - - return $data; - } - - private function get_latest_schemev(){ - Exceptions::error_rep("Getting latest database scheme version..."); - $dir = dirname(__DIR__) . "/updates/data/scheme.db"; - $d = file_get_contents($dir); - return $d; - } - - public function compare_scheme(){ - Exceptions::error_rep("Comparing database scheme versions..."); - $current = $this->request_schemev(); - $latest = $this->get_latest_schemev(); - $current1 = $current + 1; - if((int)$current < (int)$latest){ - Exceptions::error_rep("Database scheme is not up to date. Please migrate your database immediately! | Missing upgrades: {$current1}-{$latest}"); - } elseif((int)$current > (int)$latest){ - Exceptions::error_rep("You have somehow managed to be more than up to date for your database. Your database is probably defect and needs manual repair."); - } - } - - private function get_upgrade_file($scheme){ - Exceptions::error_rep("Getting upgrade file for scheme {$scheme}..."); - return __DIR__ . "/data/migrations/{$scheme}.sql"; - } - - public function perform_migration($updateToSchemeV){ - Exceptions::error_rep("Performing database migration to scheme from {$this->request_schemev()} to {$updateToSchemeV}..."); - $current = $this->request_schemev(); - $path = $this->get_upgrade_file($updateToSchemeV); - if(($current + 1) == $updateToSchemeV){ - Exceptions::error_rep("Upgrading database scheme to {$updateToSchemeV}..."); - $db = Arbeitszeit::get_app_ini()["mysql"]; - $command = "mysql --user={$db["db_user"]} --password={$db["db_password"]} -h {$db["db_host"]} -D {$db["db"]} < {$path}"; - $output = shell_exec($command); - if($output == ""){ - Exceptions::error_rep("Successfully migrated database to scheme {$updateToSchemeV}"); - return true; - } else { - Exceptions::error_rep("An error occured while migrating database: {$output}"); - return false; - } - } else { - Exceptions::error_rep("Cannot upgrade database scheme: You are missing other updates! | Tried scheme: {$updateToSchemeV} - Current: {$current}"); - return false; - } - } - } -} - - -?> diff --git a/api/v1/inc/arbeit.inc.php b/api/v1/inc/arbeit.inc.php index 2ef5814..bfad176 100644 --- a/api/v1/inc/arbeit.inc.php +++ b/api/v1/inc/arbeit.inc.php @@ -20,7 +20,7 @@ require_once dirname(__DIR__, 1) . "/class/exceptions/exceptions.arbeit.inc.php"; require_once dirname(__DIR__, 1) . "/class/vacation/vacation.arbeit.inc.php"; require_once dirname(__DIR__, 1) . "/class/sickness/sickness.arbeit.inc.php"; -require_once $_SERVER["DOCUMENT_ROOT"] . "/vendor/autoload.php"; +require_once dirname(__DIR__, 3) . "/vendor/autoload.php"; require_once dirname(__DIR__, 1) . "/class/exports/ExportModule.arbeit.inc.php"; require_once dirname(__DIR__, 1) . "/class/exports/modules/ExportModuleInterface.em.arbeit.inc.php"; @@ -34,7 +34,6 @@ require_once dirname(__DIR__, 1) . "/class/plugins/Hooks.plugins.arbeit.inc.php"; require_once dirname(__DIR__, 1) . "/class/auth/plugins/ldap/ldap.auth.arbeit.inc.php"; -require_once dirname(__DIR__, 1) . "/class/updates/updates.arbeit.inc.php"; require_once dirname(__DIR__ . 1) . "/class/mails/Mails.arbeit.inc.php"; require_once dirname(__DIR__ . 1) . "/class/mails/MailTemplateData.mails.arbeit.inc.php"; @@ -45,8 +44,4 @@ require_once dirname(__DIR__ . 1) . "/class/mails/provider/DefaultMailsProvider.mails.arbeit.inc.php"; use Arbeitszeit\Hooks; Hooks::initialize(); - -use Arbeitszeit\Updates; -$updates = new Updates; -$updates->compare_scheme(); ?> \ No newline at end of file diff --git a/composer.json b/composer.json index 12d80ee..4431903 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,8 @@ "ldaptools/ldaptools": "dev-master#69d086b137ece33a7b4e182bc90bb65dc0454630", "php": ">=8.0", - "dompdf/dompdf": "^3.0" + "dompdf/dompdf": "^3.0", + "robmorgan/phinx": "^0.16.6" }, "repositories": [ { diff --git a/migrations/migrations/20250331155531_init_az_scheme.php b/migrations/migrations/20250331155531_init_az_scheme.php new file mode 100644 index 0000000..5ec619d --- /dev/null +++ b/migrations/migrations/20250331155531_init_az_scheme.php @@ -0,0 +1,36 @@ +hasTable('arbeitszeiten'); + if ($exists) { + echo "\nSkipping. Table already exists.\n"; + return; + } + + $this->execute("SET SQL_MODE = 'NO_AUTO_VALUE_ON_ZERO';"); + $this->execute("SET time_zone = '+00:00';"); + + $this->table('arbeitszeiten') + ->addColumn('name', 'string', ['limit' => 256]) + ->addColumn('email', 'string', ['limit' => 256]) + ->addColumn('schicht_tag', 'string', ['limit' => 256]) + ->addColumn('schicht_anfang', 'string', ['limit' => 256]) + ->addColumn('schicht_ende', 'string', ['limit' => 256]) + ->addColumn('username', 'string', ['limit' => 255]) + ->addColumn('ort', 'text', ['null' => true]) + ->addColumn('active', 'boolean', ['null' => true]) + ->addColumn('review', 'boolean', ['null' => true]) + ->addColumn('type', 'string', ['limit' => 11, 'null' => true]) + ->addColumn('pause_start', 'string', ['limit' => 255, 'null' => true]) + ->addColumn('pause_end', 'string', ['limit' => 255, 'null' => true]) + ->addColumn('attachements', 'text', ['null' => true]) + ->addColumn('project', 'string', ['limit' => 255, 'null' => true]) + ->create(); + + } +} \ No newline at end of file diff --git a/migrations/migrations/20250331155756_init_notif_scheme.php b/migrations/migrations/20250331155756_init_notif_scheme.php new file mode 100644 index 0000000..9d66793 --- /dev/null +++ b/migrations/migrations/20250331155756_init_notif_scheme.php @@ -0,0 +1,27 @@ +hasTable("kalender"); + if($exists){ + echo "\nSkipping. Table already exists.\n"; + return; + } + + $this->execute("SET SQL_MODE = 'NO_AUTO_VALUE_ON_ZERO';"); + $this->table("kalender") + ->addColumn("datum", "string", ["limit" => 255]) + ->addColumn("uhrzeit", "string", ["limit" => 255]) + ->addColumn("ort", "string", ["limit" => 255]) + ->addColumn("notiz", "string", ["limit" => 255]) + ->changePrimaryKey(["id"]) + ->create(); + } +} diff --git a/migrations/migrations/20250331160130_init_sick_scheme.php b/migrations/migrations/20250331160130_init_sick_scheme.php new file mode 100644 index 0000000..5c48e1a --- /dev/null +++ b/migrations/migrations/20250331160130_init_sick_scheme.php @@ -0,0 +1,28 @@ +hasTable('sick'); + if ($exists) { + echo "\nSkipping. Table already exists.\n"; + return; + } + + $this->execute("SET SQL_MODE = 'NO_AUTO_VALUE_ON_ZERO';"); + $this->execute("SET time_zone = '+00:00';"); + $this->table("sick") + ->addColumn("username", "string", ["limit" => 255]) + ->addColumn("start", "text", ["null" => false]) + ->addColumn("stop", "text", ["null" => false]) + ->addColumn("status", "text", ["null" => false]) + ->changePrimaryKey(["id"]) + ->create(); + } +} diff --git a/migrations/migrations/20250331160400_init_vacation_scheme.php b/migrations/migrations/20250331160400_init_vacation_scheme.php new file mode 100644 index 0000000..786c076 --- /dev/null +++ b/migrations/migrations/20250331160400_init_vacation_scheme.php @@ -0,0 +1,28 @@ +hasTable('vacation'); + if ($exists) { + echo "\nSkipping. Table already exists.\n"; + return; + } + + $this->execute("SET SQL_MODE = 'NO_AUTO_VALUE_ON_ZERO';"); + $this->execute("SET time_zone = '+00:00';"); + $this->table("vacation") + ->addColumn("username", "string", ["limit" => 255]) + ->addColumn("start", "text", ["null" => false]) + ->addColumn("stop", "text", ["null" => false]) + ->addColumn("status", "text", ["null" => false]) + ->changePrimaryKey(["id"]) + ->create(); + } +} diff --git a/migrations/migrations/20250331160543_init_user_scheme.php b/migrations/migrations/20250331160543_init_user_scheme.php new file mode 100644 index 0000000..cc8abe8 --- /dev/null +++ b/migrations/migrations/20250331160543_init_user_scheme.php @@ -0,0 +1,29 @@ +hasTable('users'); + if ($exists) { + echo "\nSkipping. Table already exists.\n"; + return; + } + + $this->table("users") + ->addColumn("name", "string", ["limit" => 256]) + ->addColumn("username", "string", ["limit" => 255]) + ->addColumn("email", "string", ["limit" => 256]) + ->addColumn("password", "string", ["limit" => 256]) + ->addColumn("email_confirmed", "boolean") + ->addColumn("isAdmin", "string", ["limit" => 256]) + ->addColumn("state", "text", ["null" => true]) + ->addColumn("easymode", "boolean", ["null" => true]) + ->changePrimaryKey(["id"]) + ->create(); + } +} diff --git a/migrations/migrations/20250331162111_init_schicht_scheme.php b/migrations/migrations/20250331162111_init_schicht_scheme.php new file mode 100644 index 0000000..03f093d --- /dev/null +++ b/migrations/migrations/20250331162111_init_schicht_scheme.php @@ -0,0 +1,27 @@ +hasTable('schicht'); + if ($exists) { + echo "\nSkipping. Table already exists.\n"; + return; + } + + $this->table('schicht') + ->addColumn('name', 'string', ['limit' => 256]) + ->addColumn('email', 'string', ['limit' => 256]) + ->addColumn('schicht_gestartet_zeit', 'string', ['limit' => 256, 'null' => true]) + ->addColumn('schicht_ende_zeit', 'string', ['limit' => 256, 'null' => true]) + ->addColumn('schicht_datum', 'string', ['limit' => 256, 'null' => true]) + ->changePrimaryKey(['id']) + ->create(); + } +} diff --git a/migrations/migrations/20250331164242_init_first_user.php b/migrations/migrations/20250331164242_init_first_user.php new file mode 100644 index 0000000..22eac52 --- /dev/null +++ b/migrations/migrations/20250331164242_init_first_user.php @@ -0,0 +1,37 @@ +hasTable("users")) { + + $data = [ + 'name' => 'admin', + 'username' => 'admin', + 'email' => 'admin@admin.com', + 'password' => '$2y$10$5cmvKFvDl07C0QJJRMtG4OPSS56n.7p7VOw9UAHoIIGJTvvqp/HKG', + 'email_confirmed' => true, + 'isAdmin' => true, + 'state' => null, + 'easymode' => false, + ]; + + $users = $this->fetchRow("SELECT COUNT(*) as count FROM users WHERE username = 'admin'"); + if ($users['count'] == 0) { + $this->table('users')->insert($data)->saveData(); + } else { + echo "User count > 0, not inserting default user\n"; + } + + } else { + echo "Table users does not exist\n"; + } + } +} diff --git a/phinx.php b/phinx.php new file mode 100644 index 0000000..7fa4117 --- /dev/null +++ b/phinx.php @@ -0,0 +1,27 @@ + [ + 'migrations' => '%%PHINX_CONFIG_DIR%%/migrations/migrations', + 'seeds' => '%%PHINX_CONFIG_DIR%%/migrations/seeds' + ], + 'environments' => [ + 'default_migration_table' => 'phinxlog', + 'default_environment' => 'production', + 'production' => [ + 'adapter' => 'mysql', + 'host' => $data['mysql']['db_host'], + 'name' => $data['mysql']['db'], + 'user' => $data['mysql']['db_user'], + 'pass' => $data['mysql']['db_password'], + 'port' => '3306', + 'charset' => 'utf8', + ], + // you can add development and testing environments here + ], + 'version_order' => 'creation' +]; diff --git a/setup/sql.sql b/setup/sql.sql deleted file mode 100644 index be96fd1..0000000 --- a/setup/sql.sql +++ /dev/null @@ -1,110 +0,0 @@ -USE ab; --- Remove the above line if your database name is not 'ab' -SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; -START TRANSACTION; -SET time_zone = "+00:00"; - -CREATE TABLE `arbeitszeiten` ( - `id` int(11) NOT NULL, - `name` varchar(256) NOT NULL, - `email` varchar(256) NOT NULL, - `schicht_tag` varchar(256) NOT NULL, - `schicht_anfang` varchar(256) NOT NULL, - `schicht_ende` varchar(256) NOT NULL, - `username` varchar(255) NOT NULL, - `ort` text DEFAULT NULL, - `active` tinyint(1) DEFAULT NULL, - `review` tinyint(1) DEFAULT NULL, - `type` varchar(11) DEFAULT NULL, - `pause_start` varchar(255) DEFAULT NULL, - `pause_end` varchar(255) DEFAULT NULL, - `attachements` text DEFAULT NULL, - `project` varchar(255) DEFAULT null -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - -CREATE TABLE `kalender` ( - `id` int(11) NOT NULL, - `datum` text NOT NULL, - `uhrzeit` text NOT NULL, - `ort` text NOT NULL, - `notiz` text NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_german1_ci; - -CREATE TABLE `schicht` ( - `id` int(11) NOT NULL, - `name` varchar(256) NOT NULL, - `email` varchar(256) NOT NULL, - `schicht_gestartet_zeit` varchar(256) DEFAULT NULL, - `schicht_ende_zeit` varchar(256) DEFAULT NULL, - `schicht_datum` varchar(256) DEFAULT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - -CREATE TABLE `sick` ( - `id` int(11) NOT NULL, - `username` varchar(256) NOT NULL, - `start` text NOT NULL, - `stop` text DEFAULT NULL, - `status` text NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - -CREATE TABLE `users` ( - `id` int(11) NOT NULL, - `name` varchar(256) NOT NULL, - `username` varchar(255) NOT NULL, - `email` varchar(256) NOT NULL, - `password` varchar(256) NOT NULL, - `email_confirmed` tinyint(1) NOT NULL, - `isAdmin` varchar(256) NOT NULL, - `state` text DEFAULT NULL, - `easymode` tinyint(1) DEFAULT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - -CREATE TABLE `vacation` ( - `id` int(11) NOT NULL, - `username` varchar(256) NOT NULL, - `start` text NOT NULL, - `stop` text DEFAULT NULL, - `status` text NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - -CREATE TABLE `scheme` ( - `v` INT(11) NULL DEFAULT '1' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - -ALTER TABLE `arbeitszeiten` - ADD PRIMARY KEY (`id`); - -ALTER TABLE `kalender` - ADD PRIMARY KEY (`id`); - -ALTER TABLE `schicht` - ADD PRIMARY KEY (`id`); - -ALTER TABLE `sick` - ADD PRIMARY KEY (`id`); - -ALTER TABLE `users` - ADD PRIMARY KEY (`id`), - ADD UNIQUE KEY `email` (`email`); - -ALTER TABLE `vacation` - ADD PRIMARY KEY (`id`); - -ALTER TABLE `arbeitszeiten` - MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; - -ALTER TABLE `kalender` - MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; - -ALTER TABLE `schicht` - MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; - -ALTER TABLE `sick` - MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; - -ALTER TABLE `users` - MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; - -ALTER TABLE `vacation` - MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; -COMMIT; \ No newline at end of file diff --git a/setup/upgrade.php b/setup/upgrade.php deleted file mode 100644 index 9a345e0..0000000 --- a/setup/upgrade.php +++ /dev/null @@ -1,16 +0,0 @@ - 2, 2 -> 3, ... -$o = $updates->perform_migration($missingUpdate); - -?> \ No newline at end of file diff --git a/setup/usercreate.php b/setup/usercreate.php index bc1bf15..baf0464 100644 --- a/setup/usercreate.php +++ b/setup/usercreate.php @@ -4,6 +4,8 @@ * This script allows 3 input variables: * username, desired password and email * and returns an SQL statement to be run in the database + * + * @deprecated This script should not be used, the default user is created via phinx now */ if(isset($argv[1], $argv[2], $argv[3])){ From 3dc0733a049265ce53711ec67780876f9637abe9 Mon Sep 17 00:00:00 2001 From: Ente Date: Mon, 31 Mar 2025 20:48:19 +0200 Subject: [PATCH 23/48] TT-4: Finalizing NFCLogin plugin --- api/v1/class/arbeitszeit.inc.php | 8 ++ api/v1/class/benutzer/benutzer.arbeit.inc.php | 2 +- .../class/plugins/plugins/nfclogin/plugin.yml | 3 +- .../plugins/plugins/nfclogin/src/Main.php | 42 ++++++ .../plugins/nfclogin/src/read_nfc_block.py | 35 +++++ .../routes/readBlock4.ep.toil.arbeit.inc.php | 57 ++++++++ .../routes/writeNfc.ep.toil.arbeit.inc.php | 57 ++++++++ .../plugins/plugins/nfclogin/src/write_nfc.py | 42 ++++++ .../plugins/plugins/nfclogin/views/cards.php | 127 +++++++++++++----- 9 files changed, 340 insertions(+), 33 deletions(-) create mode 100644 api/v1/class/plugins/plugins/nfclogin/src/read_nfc_block.py create mode 100644 api/v1/class/plugins/plugins/nfclogin/src/routes/readBlock4.ep.toil.arbeit.inc.php create mode 100644 api/v1/class/plugins/plugins/nfclogin/src/routes/writeNfc.ep.toil.arbeit.inc.php create mode 100644 api/v1/class/plugins/plugins/nfclogin/src/write_nfc.py diff --git a/api/v1/class/arbeitszeit.inc.php b/api/v1/class/arbeitszeit.inc.php index c673d75..e23660b 100644 --- a/api/v1/class/arbeitszeit.inc.php +++ b/api/v1/class/arbeitszeit.inc.php @@ -704,6 +704,14 @@ public static function fix_easymode_worktime($username) } + public function blockIfNotAdmin(){ + if(!Benutzer::is_admin(Benutzer::get_user($_SESSION["username"]))){ + header("Location: /"); + } + return false; + } + + public function notifications(): Notifications { diff --git a/api/v1/class/benutzer/benutzer.arbeit.inc.php b/api/v1/class/benutzer/benutzer.arbeit.inc.php index 9e02b93..bce1fe7 100644 --- a/api/v1/class/benutzer/benutzer.arbeit.inc.php +++ b/api/v1/class/benutzer/benutzer.arbeit.inc.php @@ -259,7 +259,7 @@ public function get_user_html($username){ * @param array $user * @return bool Returns true if the user is an admin and false otherwise */ - public function is_admin($user){ + public static function is_admin($user){ if($user["isAdmin"] == true){ return true; } else { diff --git a/api/v1/class/plugins/plugins/nfclogin/plugin.yml b/api/v1/class/plugins/plugins/nfclogin/plugin.yml index 1b0f4a3..95003da 100644 --- a/api/v1/class/plugins/plugins/nfclogin/plugin.yml +++ b/api/v1/class/plugins/plugins/nfclogin/plugin.yml @@ -11,5 +11,4 @@ enabled: true custom.values: license: LIC nav_links: - 'Manage Cards': views/cards.php - 'Manage Card Settings': views/admin.php \ No newline at end of file + 'Manage Cards': views/cards.php \ No newline at end of file diff --git a/api/v1/class/plugins/plugins/nfclogin/src/Main.php b/api/v1/class/plugins/plugins/nfclogin/src/Main.php index 1ef7e14..d8ce341 100644 --- a/api/v1/class/plugins/plugins/nfclogin/src/Main.php +++ b/api/v1/class/plugins/plugins/nfclogin/src/Main.php @@ -39,6 +39,8 @@ public function setup(): void { public function register_routes(): void { Exceptions::error_rep("{$this->log_append} Registering custom routes..."); CustomRoutes::registerCustomRoute("readNfc", "/api/v1/class/plugins/plugins/nfclogin/src/routes/readNfc.ep.toil.arbeit.inc.php", 1); + CustomRoutes::registerCustomRoute("writeNfc", "/api/v1/class/plugins/plugins/nfclogin/src/routes/writeNfc.ep.toil.arbeit.inc.php", 1); + CustomRoutes::registerCustomRoute("readBlock4", "/api/v1/class/plugins/plugins/nfclogin/src/routes/readBlock4.ep.toil.arbeit.inc.php", 1); } public function set_log_append(): void { @@ -89,4 +91,44 @@ public function readCard(): ?array { return $data; } + + public function assignCard($username){ + $scriptPath = __DIR__ . "/write_nfc.py"; + exec("python3 " . escapeshellarg($scriptPath) . " " . escapeshellarg($username) . " 2>&1", $output, $status); + if ($status !== 0) { + return [ + "error" => "Helper script execution failed", + "output" => $output + ]; + } + $json = implode("", $output); + $data = json_decode($json, true); + if (json_last_error() !== JSON_ERROR_NONE) { + return [ + "error" => "Invalid JSON response from helper script", + "raw_output" => $output + ]; + } + return $data; + } + + public function readBlock4() { + $scriptPath = __DIR__ . "/read_nfc_block.py"; + exec("python3 " . escapeshellarg($scriptPath) . " 2>&1", $output, $status); + if ($status !== 0) { + return [ + "error" => "Helper script execution failed", + "output" => $output + ]; + } + $json = implode("", $output); + $data = json_decode($json, true); + if (json_last_error() !== JSON_ERROR_NONE) { + return [ + "error" => "Invalid JSON response from helper script", + "raw_output" => $output + ]; + } + return $data; + } } diff --git a/api/v1/class/plugins/plugins/nfclogin/src/read_nfc_block.py b/api/v1/class/plugins/plugins/nfclogin/src/read_nfc_block.py new file mode 100644 index 0000000..ccb9f1c --- /dev/null +++ b/api/v1/class/plugins/plugins/nfclogin/src/read_nfc_block.py @@ -0,0 +1,35 @@ +from smartcard.System import readers +from smartcard.util import toHexString + +BLOCK = 4 +KEY = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF] # Standard-Key A + +def auth_block(connection, block): + return connection.transmit([ + 0xFF, 0x86, 0x00, 0x00, 0x05, + 0x01, 0x00, 0x60, block, 0x00 + ] + KEY) + +def read_block(connection, block): + return connection.transmit([0xFF, 0xB0, 0x00, block, 0x10]) + +r = readers() +if not r: + print("No NFC reader found.") + exit(1) + +reader = r[0] +connection = reader.createConnection() +connection.connect() + +_, sw1, sw2 = auth_block(connection, BLOCK) +if sw1 != 0x90 or sw2 != 0x00: + print(f"Auth failed for block {BLOCK}") + exit(1) + +data, sw1, sw2 = read_block(connection, BLOCK) +if sw1 == 0x90 and sw2 == 0x00: + value = bytearray(data).decode('utf-8', errors='ignore').rstrip('\x00') + print(f'{{"block": {BLOCK}, "value": "{value}"}}') +else: + print("Read failed.") diff --git a/api/v1/class/plugins/plugins/nfclogin/src/routes/readBlock4.ep.toil.arbeit.inc.php b/api/v1/class/plugins/plugins/nfclogin/src/routes/readBlock4.ep.toil.arbeit.inc.php new file mode 100644 index 0000000..be0ecc4 --- /dev/null +++ b/api/v1/class/plugins/plugins/nfclogin/src/routes/readBlock4.ep.toil.arbeit.inc.php @@ -0,0 +1,57 @@ +$name = $value; + } + + public function __get($name) + { + return $this->$name; + } + + public function get() + { + header('Content-Type: application/json'); + + try { + $nfc = new NFClogin; + $data = $nfc->readBlock4(); + echo json_encode($data); + } catch (\Exception $e) { + echo json_encode(["error" => true, "message" => $e->getMessage()]); + } + } + + public function post($post = null) + { + // Optional + } + + public function delete() + { + // Optional + } + + public function put() + { + // Optional + } + } +} diff --git a/api/v1/class/plugins/plugins/nfclogin/src/routes/writeNfc.ep.toil.arbeit.inc.php b/api/v1/class/plugins/plugins/nfclogin/src/routes/writeNfc.ep.toil.arbeit.inc.php new file mode 100644 index 0000000..0c15911 --- /dev/null +++ b/api/v1/class/plugins/plugins/nfclogin/src/routes/writeNfc.ep.toil.arbeit.inc.php @@ -0,0 +1,57 @@ +$name = $value; + } + + public function __get($name) + { + return $this->$name; + } + + public function get() + { + header('Content-Type: application/json'); + + try { + $nfc = new NFClogin; + $data = $nfc->assignCard($_GET["username"]); + echo json_encode($data); + } catch (\Exception $e) { + echo json_encode(["error" => true, "message" => $e->getMessage()]); + } + } + + public function post($post = null) + { + // Optional + } + + public function delete() + { + // Optional + } + + public function put() + { + // Optional + } + } +} diff --git a/api/v1/class/plugins/plugins/nfclogin/src/write_nfc.py b/api/v1/class/plugins/plugins/nfclogin/src/write_nfc.py new file mode 100644 index 0000000..69d84b6 --- /dev/null +++ b/api/v1/class/plugins/plugins/nfclogin/src/write_nfc.py @@ -0,0 +1,42 @@ +import sys +from smartcard.System import readers + +if len(sys.argv) != 2: + print("Usage: python3 write_to_card.py ") + sys.exit(1) + +input_data = sys.argv[1] + +BLOCK = 4 +KEY = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF] # Default Key A + +def auth_block(connection, block): + return connection.transmit([ + 0xFF, 0x86, 0x00, 0x00, 0x05, + 0x01, 0x00, 0x60, block, 0x00 + ] + KEY) + +def write_block(connection, block, data_str): + data = bytearray(data_str.encode("utf-8")[:16]) + data += b"\x00" * (16 - len(data)) + return connection.transmit([0xFF, 0xD6, 0x00, block, 0x10] + list(data)) + +r = readers() +if not r: + print("No NFC reader found.") + sys.exit(1) + +reader = r[0] +connection = reader.createConnection() +connection.connect() + +_, sw1, sw2 = auth_block(connection, BLOCK) +if sw1 != 0x90 or sw2 != 0x00: + print(f"Auth failed for block {BLOCK}.") + sys.exit(1) + +_, sw1, sw2 = write_block(connection, BLOCK, input_data) +if sw1 == 0x90 and sw2 == 0x00: + print(f"Block {BLOCK} successfully written with value: '{input_data}'") +else: + print('{"error": "write failed"}') diff --git a/api/v1/class/plugins/plugins/nfclogin/views/cards.php b/api/v1/class/plugins/plugins/nfclogin/views/cards.php index 2bdded3..13fdb65 100644 --- a/api/v1/class/plugins/plugins/nfclogin/views/cards.php +++ b/api/v1/class/plugins/plugins/nfclogin/views/cards.php @@ -9,60 +9,127 @@ $arbeit = new Arbeitszeit; $main = new NFClogin; +$arbeit->auth()->login_validation(); +$arbeit->blockIfNotAdmin(); ?> +

NFCLogin Plugin

-

Please reload the page while holding the NFC card to read it.

- - -
-

- readCard(); +function writeNFC() { + const username = document.getElementById('nfc-username').value; + if (!username) return false; - if ($cardData) { - echo "Output/UUID

";
-            echo htmlspecialchars(json_encode($cardData, JSON_PRETTY_PRINT));
-            echo "
"; + showModal("Writing to card..."); + fetch('/api/v1/toil/writeNfc', { + method: "POST", + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ username: username }) + }) + .then(res => res.json()) + .then(data => { + if (data.success || data.uid) { + document.getElementById('nfc-status').textContent = "Successfully written to card."; + setTimeout(() => location.reload(), 10000); } else { - echo "No card detected."; + document.getElementById('nfc-status').textContent = "Error while writing to card."; + hideModal(); } - } catch (Exception $e) { - echo "Error: " . $e->getMessage(); - } - ?> -

-
+ }) + .catch(err => { + console.error(err); + document.getElementById('nfc-status').textContent = "Error while writing to card."; + hideModal(); + }); + + return false; +} + +function readBlock4() { + showModal("Reading user block..."); + fetch('/api/v1/toil/readBlock4') + .then(res => res.json()) + .then(data => { + if (data.block) { + document.getElementById('nfc-status').textContent = "Block 4: " + data.block; + } else { + document.getElementById('nfc-status').textContent = "Error while reading block 4."; + } + hideModal(3000); + }) + .catch(err => { + console.error(err); + document.getElementById('nfc-status').textContent = "Error while reading."; + hideModal(); + }); +} + From 557e380d6966661bcbc7a55304e8b3854a13510c Mon Sep 17 00:00:00 2001 From: Ente Date: Mon, 31 Mar 2025 20:57:31 +0200 Subject: [PATCH 24/48] static isAdmin function --- api/v1/class/benutzer/benutzer.arbeit.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/v1/class/benutzer/benutzer.arbeit.inc.php b/api/v1/class/benutzer/benutzer.arbeit.inc.php index e76a9b6..ab3613a 100644 --- a/api/v1/class/benutzer/benutzer.arbeit.inc.php +++ b/api/v1/class/benutzer/benutzer.arbeit.inc.php @@ -282,7 +282,7 @@ public function get_user_html($username) * @param array $user * @return bool Returns true if the user is an admin and false otherwise */ - public function is_admin($user) + public static function is_admin($user) { if ($user["isAdmin"] == true) { return true; From 6826516ba6b6c6cdbf108ac059764ce0b7186ba9 Mon Sep 17 00:00:00 2001 From: Ente Date: Mon, 31 Mar 2025 21:09:48 +0200 Subject: [PATCH 25/48] Remove auth for NFC write --- .../plugins/nfclogin/src/read_nfc_block.py | 18 ++-------- .../plugins/plugins/nfclogin/src/write_nfc.py | 35 ++++++------------- 2 files changed, 13 insertions(+), 40 deletions(-) diff --git a/api/v1/class/plugins/plugins/nfclogin/src/read_nfc_block.py b/api/v1/class/plugins/plugins/nfclogin/src/read_nfc_block.py index ccb9f1c..2ace8f2 100644 --- a/api/v1/class/plugins/plugins/nfclogin/src/read_nfc_block.py +++ b/api/v1/class/plugins/plugins/nfclogin/src/read_nfc_block.py @@ -2,16 +2,6 @@ from smartcard.util import toHexString BLOCK = 4 -KEY = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF] # Standard-Key A - -def auth_block(connection, block): - return connection.transmit([ - 0xFF, 0x86, 0x00, 0x00, 0x05, - 0x01, 0x00, 0x60, block, 0x00 - ] + KEY) - -def read_block(connection, block): - return connection.transmit([0xFF, 0xB0, 0x00, block, 0x10]) r = readers() if not r: @@ -22,14 +12,10 @@ def read_block(connection, block): connection = reader.createConnection() connection.connect() -_, sw1, sw2 = auth_block(connection, BLOCK) -if sw1 != 0x90 or sw2 != 0x00: - print(f"Auth failed for block {BLOCK}") - exit(1) +data, sw1, sw2 = connection.transmit([0xFF, 0xB0, 0x00, BLOCK, 0x10]) -data, sw1, sw2 = read_block(connection, BLOCK) if sw1 == 0x90 and sw2 == 0x00: value = bytearray(data).decode('utf-8', errors='ignore').rstrip('\x00') print(f'{{"block": {BLOCK}, "value": "{value}"}}') else: - print("Read failed.") + print('{"error": "Read failed", "sw1": %d, "sw2": %d}' % (sw1, sw2)) diff --git a/api/v1/class/plugins/plugins/nfclogin/src/write_nfc.py b/api/v1/class/plugins/plugins/nfclogin/src/write_nfc.py index 69d84b6..1a50e1e 100644 --- a/api/v1/class/plugins/plugins/nfclogin/src/write_nfc.py +++ b/api/v1/class/plugins/plugins/nfclogin/src/write_nfc.py @@ -1,42 +1,29 @@ import sys from smartcard.System import readers +from smartcard.util import toBytes + +BLOCK = 4 if len(sys.argv) != 2: - print("Usage: python3 write_to_card.py ") + print("Usage: python3 write_ultralight_block.py ") sys.exit(1) -input_data = sys.argv[1] - -BLOCK = 4 -KEY = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF] # Default Key A - -def auth_block(connection, block): - return connection.transmit([ - 0xFF, 0x86, 0x00, 0x00, 0x05, - 0x01, 0x00, 0x60, block, 0x00 - ] + KEY) - -def write_block(connection, block, data_str): - data = bytearray(data_str.encode("utf-8")[:16]) - data += b"\x00" * (16 - len(data)) - return connection.transmit([0xFF, 0xD6, 0x00, block, 0x10] + list(data)) +data = sys.argv[1][:4].ljust(4, '\x00') +data_bytes = toBytes(' '.join([f"{ord(c):02X}" for c in data])) r = readers() if not r: - print("No NFC reader found.") + print('{"error": "No reader found"}') sys.exit(1) reader = r[0] connection = reader.createConnection() connection.connect() -_, sw1, sw2 = auth_block(connection, BLOCK) -if sw1 != 0x90 or sw2 != 0x00: - print(f"Auth failed for block {BLOCK}.") - sys.exit(1) +command = [0xFF, 0xD6, 0x00, BLOCK, 0x04] + data_bytes +response, sw1, sw2 = connection.transmit(command) -_, sw1, sw2 = write_block(connection, BLOCK, input_data) if sw1 == 0x90 and sw2 == 0x00: - print(f"Block {BLOCK} successfully written with value: '{input_data}'") + print(f'{{"success": true, "block": {BLOCK}, "written": "{data}"}}') else: - print('{"error": "write failed"}') + print(f'{{"error": "Write failed", "sw1": "{sw1:02X}", "sw2": "{sw2:02X}"}}') From d3e668661439dc33f02242ac07681160c84848ce Mon Sep 17 00:00:00 2001 From: Ente Date: Mon, 31 Mar 2025 21:16:17 +0200 Subject: [PATCH 26/48] Fix JSON and JS for nfclogin --- .../plugins/nfclogin/src/read_nfc_block.py | 2 +- .../plugins/plugins/nfclogin/views/cards.php | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/api/v1/class/plugins/plugins/nfclogin/src/read_nfc_block.py b/api/v1/class/plugins/plugins/nfclogin/src/read_nfc_block.py index 2ace8f2..1475c13 100644 --- a/api/v1/class/plugins/plugins/nfclogin/src/read_nfc_block.py +++ b/api/v1/class/plugins/plugins/nfclogin/src/read_nfc_block.py @@ -16,6 +16,6 @@ if sw1 == 0x90 and sw2 == 0x00: value = bytearray(data).decode('utf-8', errors='ignore').rstrip('\x00') - print(f'{{"block": {BLOCK}, "value": "{value}"}}') + print('{"value": "' + value + '"}') else: print('{"error": "Read failed", "sw1": %d, "sw2": %d}' % (sw1, sw2)) diff --git a/api/v1/class/plugins/plugins/nfclogin/views/cards.php b/api/v1/class/plugins/plugins/nfclogin/views/cards.php index 13fdb65..4e694fa 100644 --- a/api/v1/class/plugins/plugins/nfclogin/views/cards.php +++ b/api/v1/class/plugins/plugins/nfclogin/views/cards.php @@ -74,7 +74,7 @@ function startNFC() { document.getElementById('nfc-status').textContent = "Card detected: " + data.uid; setTimeout(() => location.reload(), 1000); } else { - document.getElementById('nfc-status').textContent = "No card detected."; + document.getElementById('nfc-status').textContent = data.error || "No card detected."; hideModal(); } }) @@ -97,11 +97,11 @@ function writeNFC() { }) .then(res => res.json()) .then(data => { - if (data.success || data.uid) { - document.getElementById('nfc-status').textContent = "Successfully written to card."; - setTimeout(() => location.reload(), 10000); + if (data.success) { + document.getElementById('nfc-status').textContent = "Successfully block: " + (data.block || username); + setTimeout(() => location.reload(), 2000); } else { - document.getElementById('nfc-status').textContent = "Error while writing to card."; + document.getElementById('nfc-status').textContent = data.error || "Write failed."; hideModal(); } }) @@ -119,17 +119,18 @@ function readBlock4() { fetch('/api/v1/toil/readBlock4') .then(res => res.json()) .then(data => { - if (data.block) { - document.getElementById('nfc-status').textContent = "Block 4: " + data.block; + if (data.value) { + document.getElementById('nfc-status').textContent = "Block 4: " + data.value; } else { - document.getElementById('nfc-status').textContent = "Error while reading block 4."; + document.getElementById('nfc-status').textContent = data.error || "Read failed."; } hideModal(3000); }) .catch(err => { console.error(err); - document.getElementById('nfc-status').textContent = "Error while reading."; + document.getElementById('nfc-status').textContent = "Error while reading block."; hideModal(); }); } + From d1f26c77d7e7c70af6d070c31c117b9c0cb77d40 Mon Sep 17 00:00:00 2001 From: Ente Date: Mon, 31 Mar 2025 21:23:37 +0200 Subject: [PATCH 27/48] Fix nfclogin --- api/v1/class/plugins/plugins/nfclogin/src/Main.php | 4 ++-- .../nfclogin/src/routes/readBlock4.ep.toil.arbeit.inc.php | 4 ++++ api/v1/class/plugins/plugins/nfclogin/views/cards.php | 6 ++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/api/v1/class/plugins/plugins/nfclogin/src/Main.php b/api/v1/class/plugins/plugins/nfclogin/src/Main.php index d8ce341..b14ad41 100644 --- a/api/v1/class/plugins/plugins/nfclogin/src/Main.php +++ b/api/v1/class/plugins/plugins/nfclogin/src/Main.php @@ -92,9 +92,9 @@ public function readCard(): ?array { return $data; } - public function assignCard($username){ + public function assignCard(int $id){ $scriptPath = __DIR__ . "/write_nfc.py"; - exec("python3 " . escapeshellarg($scriptPath) . " " . escapeshellarg($username) . " 2>&1", $output, $status); + exec("python3 " . escapeshellarg($scriptPath) . " " . escapeshellarg((string)$id) . " 2>&1", $output, $status); if ($status !== 0) { return [ "error" => "Helper script execution failed", diff --git a/api/v1/class/plugins/plugins/nfclogin/src/routes/readBlock4.ep.toil.arbeit.inc.php b/api/v1/class/plugins/plugins/nfclogin/src/routes/readBlock4.ep.toil.arbeit.inc.php index be0ecc4..58b7d6d 100644 --- a/api/v1/class/plugins/plugins/nfclogin/src/routes/readBlock4.ep.toil.arbeit.inc.php +++ b/api/v1/class/plugins/plugins/nfclogin/src/routes/readBlock4.ep.toil.arbeit.inc.php @@ -7,6 +7,7 @@ use Toil\EP; use Arbeitszeit\Arbeitszeit; + use Arbeitszeit\Benutzer; use NFClogin\NFClogin; class readBlock4 implements EPInterface @@ -33,6 +34,9 @@ public function get() try { $nfc = new NFClogin; $data = $nfc->readBlock4(); + if(isset($data["value"])) { + $data["username"] = Benutzer::get_user_from_id($data["value"])["username"] ?? null; + } echo json_encode($data); } catch (\Exception $e) { echo json_encode(["error" => true, "message" => $e->getMessage()]); diff --git a/api/v1/class/plugins/plugins/nfclogin/views/cards.php b/api/v1/class/plugins/plugins/nfclogin/views/cards.php index 4e694fa..8540bf3 100644 --- a/api/v1/class/plugins/plugins/nfclogin/views/cards.php +++ b/api/v1/class/plugins/plugins/nfclogin/views/cards.php @@ -90,10 +90,8 @@ function writeNFC() { if (!username) return false; showModal("Writing to card..."); - fetch('/api/v1/toil/writeNfc', { - method: "POST", - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({ username: username }) + fetch('/api/v1/toil/writeNfc?username=' + username, { + method: "GET" }) .then(res => res.json()) .then(data => { From 34b56b2d91ad8989272322ad7cb8901be53bda7b Mon Sep 17 00:00:00 2001 From: Ente Date: Mon, 31 Mar 2025 21:27:14 +0200 Subject: [PATCH 28/48] Fix incorrect type --- .../nfclogin/src/routes/writeNfc.ep.toil.arbeit.inc.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/v1/class/plugins/plugins/nfclogin/src/routes/writeNfc.ep.toil.arbeit.inc.php b/api/v1/class/plugins/plugins/nfclogin/src/routes/writeNfc.ep.toil.arbeit.inc.php index 0c15911..3beb92c 100644 --- a/api/v1/class/plugins/plugins/nfclogin/src/routes/writeNfc.ep.toil.arbeit.inc.php +++ b/api/v1/class/plugins/plugins/nfclogin/src/routes/writeNfc.ep.toil.arbeit.inc.php @@ -8,6 +8,7 @@ use Toil\EP; use Arbeitszeit\Arbeitszeit; use NFClogin\NFClogin; + use Arbeitszeit\Benutzer; class writeNfc implements EPInterface { @@ -32,7 +33,8 @@ public function get() try { $nfc = new NFClogin; - $data = $nfc->assignCard($_GET["username"]); + $user = Benutzer::get_user($_GET["username"]); + $data = $nfc->assignCard($user["id"]); echo json_encode($data); } catch (\Exception $e) { echo json_encode(["error" => true, "message" => $e->getMessage()]); From b6b6f2d9a1b38b3813bb2ab8538ca725dff7d574 Mon Sep 17 00:00:00 2001 From: Ente Date: Mon, 31 Mar 2025 21:27:50 +0200 Subject: [PATCH 29/48] Fix incorrect type --- .../nfclogin/src/routes/writeNfc.ep.toil.arbeit.inc.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/api/v1/class/plugins/plugins/nfclogin/src/routes/writeNfc.ep.toil.arbeit.inc.php b/api/v1/class/plugins/plugins/nfclogin/src/routes/writeNfc.ep.toil.arbeit.inc.php index 3beb92c..685dfe1 100644 --- a/api/v1/class/plugins/plugins/nfclogin/src/routes/writeNfc.ep.toil.arbeit.inc.php +++ b/api/v1/class/plugins/plugins/nfclogin/src/routes/writeNfc.ep.toil.arbeit.inc.php @@ -34,7 +34,11 @@ public function get() try { $nfc = new NFClogin; $user = Benutzer::get_user($_GET["username"]); - $data = $nfc->assignCard($user["id"]); + if(!$user){ + echo json_encode(["error" => true, "message" => "User not found"]); + return; + } + $data = $nfc->assignCard((int)$user["id"]); echo json_encode($data); } catch (\Exception $e) { echo json_encode(["error" => true, "message" => $e->getMessage()]); From b95724f7394543861f553d5d1ab482944687b034 Mon Sep 17 00:00:00 2001 From: Ente Date: Mon, 31 Mar 2025 22:06:24 +0200 Subject: [PATCH 30/48] Integrate NFClogin into GUI --- api/v1/class/auth/auth.arbeit.inc.php | 8 +++ .../plugins/plugins/nfclogin/src/Main.php | 30 ++++++++ .../plugins/nfclogin/src/data/map.json | 0 .../routes/nfcclogin.ep.toil.arbeit.inc.php | 69 +++++++++++++++++++ .../plugins/plugins/nfclogin/views/cards.php | 3 + suite/login.php | 13 ++++ 6 files changed, 123 insertions(+) create mode 100644 api/v1/class/plugins/plugins/nfclogin/src/data/map.json create mode 100644 api/v1/class/plugins/plugins/nfclogin/src/routes/nfcclogin.ep.toil.arbeit.inc.php diff --git a/api/v1/class/auth/auth.arbeit.inc.php b/api/v1/class/auth/auth.arbeit.inc.php index 7686eb9..9f939e1 100644 --- a/api/v1/class/auth/auth.arbeit.inc.php +++ b/api/v1/class/auth/auth.arbeit.inc.php @@ -13,6 +13,9 @@ public function __construct(){ } public static function login($username, $password, $option){ # "option"-> array [ "remember" => true/false, ... ] + if($option["nfclogin"] == true){ + goto nfclogin; + } Exceptions::error_rep("Logging in user '$username'..."); $db = new DB; session_start(); @@ -81,7 +84,12 @@ public static function login($username, $password, $option){ # "option"-> array } if(password_verify($password, $data["password"])){ + if($option["nfclogin"]){ + nfclogin: + Exceptions::error_rep("Authenticated user via NFC login '" . $username . "'"); + } Exceptions::error_rep("Successfully authenticated user '" . $username . "'"); + $ini = Arbeitszeit::get_app_ini(); $ts = time(); $_SESSION["logged_in"] = true; $_SESSION["username"] = $username; diff --git a/api/v1/class/plugins/plugins/nfclogin/src/Main.php b/api/v1/class/plugins/plugins/nfclogin/src/Main.php index b14ad41..2434f44 100644 --- a/api/v1/class/plugins/plugins/nfclogin/src/Main.php +++ b/api/v1/class/plugins/plugins/nfclogin/src/Main.php @@ -41,6 +41,7 @@ public function register_routes(): void { CustomRoutes::registerCustomRoute("readNfc", "/api/v1/class/plugins/plugins/nfclogin/src/routes/readNfc.ep.toil.arbeit.inc.php", 1); CustomRoutes::registerCustomRoute("writeNfc", "/api/v1/class/plugins/plugins/nfclogin/src/routes/writeNfc.ep.toil.arbeit.inc.php", 1); CustomRoutes::registerCustomRoute("readBlock4", "/api/v1/class/plugins/plugins/nfclogin/src/routes/readBlock4.ep.toil.arbeit.inc.php", 1); + CustomRoutes::registerCustomRoute("nfcclogin", "/api/v1/class/plugins/plugins/nfclogin/src/routes/nfcclogin.ep.toil.arbeit.inc.php", 1); } public function set_log_append(): void { @@ -92,6 +93,29 @@ public function readCard(): ?array { return $data; } + public function memorize($id, $user){ + $data = json_decode(file_get_contents(__DIR__ . "/data/map.json"), true); + if ($data === null) { + $data = []; + } + $data[$id] = $user; + file_put_contents(__DIR__ . "/data/map.json", json_encode($data, JSON_PRETTY_PRINT)); + return $data; + } + + public function getUser($id){ + $data = json_decode(file_get_contents(__DIR__ . "/data/map.json"), true); + if ($data === null) { + return null; + } + // return array("id" => $user); + if (isset($data[$id])) { + return $data[$id]; + } else { + return null; + } + } + public function assignCard(int $id){ $scriptPath = __DIR__ . "/write_nfc.py"; exec("python3 " . escapeshellarg($scriptPath) . " " . escapeshellarg((string)$id) . " 2>&1", $output, $status); @@ -101,6 +125,7 @@ public function assignCard(int $id){ "output" => $output ]; } + $this->memorize($this->readCard()["uid"], Benutzer::get_user_from_id($id)); $json = implode("", $output); $data = json_decode($json, true); if (json_last_error() !== JSON_ERROR_NONE) { @@ -131,4 +156,9 @@ public function readBlock4() { } return $data; } + + public function nfcloginHtml(){ + $html = "Login with NFC"; + return $html; + } } diff --git a/api/v1/class/plugins/plugins/nfclogin/src/data/map.json b/api/v1/class/plugins/plugins/nfclogin/src/data/map.json new file mode 100644 index 0000000..e69de29 diff --git a/api/v1/class/plugins/plugins/nfclogin/src/routes/nfcclogin.ep.toil.arbeit.inc.php b/api/v1/class/plugins/plugins/nfclogin/src/routes/nfcclogin.ep.toil.arbeit.inc.php new file mode 100644 index 0000000..15fa528 --- /dev/null +++ b/api/v1/class/plugins/plugins/nfclogin/src/routes/nfcclogin.ep.toil.arbeit.inc.php @@ -0,0 +1,69 @@ +$name = $value; + } + + public function __get($name) + { + return $this->$name; + } + + public function get() + { + header('Content-Type: application/json'); + + try { + $nfc = new NFClogin; + $block = $nfc->readBlock4(); + $uid = $nfc->readCard()["uid"] ?? null; + $val = $block["value"]; + $getMapUser = $nfc->getUser($uid); + $dbUser = Benutzer::get_user_from_id($val)["username"] ?? null; + if($getMapUser == $dbUser){ + Auth::login($dbUser, "", ["nfclogin" => true]); + } else { + header("Location: /suite/login.php?error=wrongdata&uid={$uid}&block={$val}"); + exit; + } + + } catch (\Exception $e) { + echo json_encode(["error" => true, "message" => $e->getMessage()]); + } + } + + public function post($post = null) + { + // Optional + } + + public function delete() + { + // Optional + } + + public function put() + { + // Optional + } + } +} diff --git a/api/v1/class/plugins/plugins/nfclogin/views/cards.php b/api/v1/class/plugins/plugins/nfclogin/views/cards.php index 8540bf3..18a0384 100644 --- a/api/v1/class/plugins/plugins/nfclogin/views/cards.php +++ b/api/v1/class/plugins/plugins/nfclogin/views/cards.php @@ -119,6 +119,9 @@ function readBlock4() { .then(data => { if (data.value) { document.getElementById('nfc-status').textContent = "Block 4: " + data.value; + if (data.username) { + document.getElementById('nfc-status').innerHTML = "USER VERIFICATION OK " + data.username; + } } else { document.getElementById('nfc-status').textContent = data.error || "Read failed."; } diff --git a/suite/login.php b/suite/login.php index 4b771b1..fea7d45 100644 --- a/suite/login.php +++ b/suite/login.php @@ -1,8 +1,11 @@ get_app_ini(); $base_url = $ini["general"]["base_url"]; echo $arbeit->check_status_code($_SERVER["REQUEST_URI"]); @@ -27,6 +30,16 @@
+ read_plugin_configuration("nfclogin")["enabled"] == "true"){ + require_once dirname(__DIR__, 1) . "/api/v1/class/plugins/plugins/nfclogin/src/Main.php"; + $nfc = new NFClogin; + echo "
" . $nfc->nfcloginHtml() . "
"; + } + + + ?> \ No newline at end of file From fa67af669bd72f14870801ab70090e4d67a0e671 Mon Sep 17 00:00:00 2001 From: Ente Date: Mon, 31 Mar 2025 22:10:53 +0200 Subject: [PATCH 31/48] fix for mapping --- api/v1/class/plugins/plugins/nfclogin/src/Main.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/v1/class/plugins/plugins/nfclogin/src/Main.php b/api/v1/class/plugins/plugins/nfclogin/src/Main.php index 2434f44..b28b471 100644 --- a/api/v1/class/plugins/plugins/nfclogin/src/Main.php +++ b/api/v1/class/plugins/plugins/nfclogin/src/Main.php @@ -98,7 +98,7 @@ public function memorize($id, $user){ if ($data === null) { $data = []; } - $data[$id] = $user; + $data[$id] = $user["username"]; file_put_contents(__DIR__ . "/data/map.json", json_encode($data, JSON_PRETTY_PRINT)); return $data; } From f5e92a5fc0a51c10edeb51b72ae7185fa37e4c71 Mon Sep 17 00:00:00 2001 From: Ente Date: Mon, 31 Mar 2025 22:17:53 +0200 Subject: [PATCH 32/48] Fix authentication flow --- api/v1/class/auth/auth.arbeit.inc.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/api/v1/class/auth/auth.arbeit.inc.php b/api/v1/class/auth/auth.arbeit.inc.php index 9f939e1..7f22ef1 100644 --- a/api/v1/class/auth/auth.arbeit.inc.php +++ b/api/v1/class/auth/auth.arbeit.inc.php @@ -13,16 +13,15 @@ public function __construct(){ } public static function login($username, $password, $option){ # "option"-> array [ "remember" => true/false, ... ] - if($option["nfclogin"] == true){ - goto nfclogin; - } + Exceptions::error_rep("Logging in user '$username'..."); $db = new DB; session_start(); $ini = Arbeitszeit::get_app_ini(); $base_url = $ini["general"]["base_url"]; + $username = preg_replace("/\s+/", "", $username); - if($ini["ldap"]["ldap"] == "true" && !isset($option["LOCAL"])){ + if($ini["ldap"]["ldap"] == "true" && !isset($option["LOCAL"]) && !isset($option["nfclogin"])){ if(LDAP::authenticate($username, $password) != true){ Exceptions::error_rep("Login failed for username '$username' - Could not authenticate user via LDAP. See errors from before to find issue."); die(header("Location: http://{$ini["general"]["base_url"]}/suite/login.php?error=ldapauth")); @@ -31,7 +30,6 @@ public static function login($username, $password, $option){ # "option"-> array $ldap = true; } } - $username = preg_replace("/\s+/", "", $username); if(!isset($username, $password)){ Exceptions::error_rep("Login failed for username '$username' - no data supplied. Redirecting..."); die(header("Location: http://{$ini["general"]["base_url"]}/suite/login.php?error=nodata")); @@ -82,7 +80,9 @@ public static function login($username, $password, $option){ # "option"-> array } die(); } - + if(isset($option["nfclogin"])){ + goto nfclogin; + } if(password_verify($password, $data["password"])){ if($option["nfclogin"]){ nfclogin: From 2ac624e94b40264e0fbc4a9a4c47978c1bb073e9 Mon Sep 17 00:00:00 2001 From: Ente Date: Mon, 31 Mar 2025 22:24:51 +0200 Subject: [PATCH 33/48] Fix auth; add card overview --- api/v1/class/plugins/plugins/nfclogin/src/Main.php | 14 ++++++++++++++ .../class/plugins/plugins/nfclogin/views/cards.php | 6 +++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/api/v1/class/plugins/plugins/nfclogin/src/Main.php b/api/v1/class/plugins/plugins/nfclogin/src/Main.php index b28b471..d33bb5f 100644 --- a/api/v1/class/plugins/plugins/nfclogin/src/Main.php +++ b/api/v1/class/plugins/plugins/nfclogin/src/Main.php @@ -161,4 +161,18 @@ public function nfcloginHtml(){ $html = "Login with NFC"; return $html; } + + public function allCardAssignmentsHtml(){ + $data = json_decode(file_get_contents(__DIR__ . "/data/map.json"), true); + if ($data === null) { + return "No cards."; + } + $html = ""; + $html .= ""; + foreach ($data as $id => $user) { + $html .= ""; + } + $html .= "
Card IDUsername
{$id}{$user}
"; + return $html; + } } diff --git a/api/v1/class/plugins/plugins/nfclogin/views/cards.php b/api/v1/class/plugins/plugins/nfclogin/views/cards.php index 18a0384..cd85f82 100644 --- a/api/v1/class/plugins/plugins/nfclogin/views/cards.php +++ b/api/v1/class/plugins/plugins/nfclogin/views/cards.php @@ -48,7 +48,11 @@ echo "Error: " . $e->getMessage(); } ?> -

+


+
+

All Cards

+ allCardAssignmentsHtml(); ?> +