From aca01c8aa2607777205226a86be29df338eb51d9 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Mon, 27 Jan 2025 15:59:50 +0100 Subject: [PATCH 1/3] [Web] Separate Login pages --- data/web/{debug.php => admin/dashboard.php} | 17 +- data/web/admin/index.php | 29 ++ data/web/{ => admin}/mailbox.php | 16 +- data/web/{ => admin}/queue.php | 15 +- data/web/{admin.php => admin/system.php} | 13 +- data/web/css/site/admin.css | 3 - data/web/domainadmin/index.php | 28 ++ data/web/domainadmin/mailbox.php | 58 ++++ data/web/domainadmin/user.php | 44 +++ data/web/inc/functions.inc.php | 1 + data/web/inc/init_db.inc.php | 2 +- data/web/inc/prerequisites.inc.php | 2 +- data/web/inc/sessions.inc.php | 19 +- data/web/inc/triggers.admin.inc.php | 93 +++++++ data/web/inc/triggers.domainadmin.inc.php | 62 +++++ data/web/inc/triggers.global.inc.php | 48 ++++ data/web/inc/triggers.inc.php | 263 ------------------ data/web/inc/triggers.user.inc.php | 132 +++++++++ data/web/index.php | 19 +- data/web/js/site/{debug.js => dashboard.js} | 0 data/web/js/site/mailbox.js | 2 +- data/web/reset-password.php | 4 +- data/web/templates/admin_index.twig | 91 ++++++ data/web/templates/base.twig | 43 +-- .../templates/{debug.twig => dashboard.twig} | 4 +- data/web/templates/domainadmin_index.twig | 91 ++++++ data/web/templates/user/tab-user-auth.twig | 4 +- .../templates/{index.twig => user_index.twig} | 0 data/web/user.php | 33 +-- 29 files changed, 798 insertions(+), 338 deletions(-) rename data/web/{debug.php => admin/dashboard.php} (83%) create mode 100644 data/web/admin/index.php rename data/web/{ => admin}/mailbox.php (73%) rename data/web/{ => admin}/queue.php (55%) rename data/web/{admin.php => admin/system.php} (90%) create mode 100644 data/web/domainadmin/index.php create mode 100644 data/web/domainadmin/mailbox.php create mode 100644 data/web/domainadmin/user.php create mode 100644 data/web/inc/triggers.admin.inc.php create mode 100644 data/web/inc/triggers.domainadmin.inc.php create mode 100644 data/web/inc/triggers.global.inc.php delete mode 100644 data/web/inc/triggers.inc.php create mode 100644 data/web/inc/triggers.user.inc.php rename data/web/js/site/{debug.js => dashboard.js} (100%) create mode 100644 data/web/templates/admin_index.twig rename data/web/templates/{debug.twig => dashboard.twig} (99%) create mode 100644 data/web/templates/domainadmin_index.twig rename data/web/templates/{index.twig => user_index.twig} (100%) diff --git a/data/web/debug.php b/data/web/admin/dashboard.php similarity index 83% rename from data/web/debug.php rename to data/web/admin/dashboard.php index 4a099cb6e..894b1c645 100644 --- a/data/web/debug.php +++ b/data/web/admin/dashboard.php @@ -1,8 +1,17 @@ Get('LICENSE_STATUS_CAC $_SESSION['gal'] = json_decode($license_cache, true); } -$js_minifier->add('/web/js/site/debug.js'); +$js_minifier->add('/web/js/site/dashboard.js'); // vmail df $exec_fields = array('cmd' => 'system', 'task' => 'df', 'dir' => '/var/vmail'); @@ -61,7 +70,7 @@ foreach ($containers_info as $container => $container_info) { $hostname = getenv('MAILCOW_HOSTNAME'); $timezone = getenv('TZ'); -$template = 'debug.twig'; +$template = 'dashboard.twig'; $template_data = [ 'log_lines' => getenv('LOG_LINES'), 'vmail_df' => $vmail_df, diff --git a/data/web/admin/index.php b/data/web/admin/index.php new file mode 100644 index 000000000..05ba70337 --- /dev/null +++ b/data/web/admin/index.php @@ -0,0 +1,29 @@ + @$_SESSION['ldelay'] +]; + +$js_minifier->add('/web/js/site/index.js'); +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php'; diff --git a/data/web/mailbox.php b/data/web/admin/mailbox.php similarity index 73% rename from data/web/mailbox.php rename to data/web/admin/mailbox.php index a84e32c47..d0073bbd6 100644 --- a/data/web/mailbox.php +++ b/data/web/admin/mailbox.php @@ -1,10 +1,20 @@ add('/web/js/site/mailbox.js'); $js_minifier->add('/web/js/presets/sieveMailbox.js'); $js_minifier->add('/web/js/site/pwgen.js'); -$role = ($_SESSION['mailcow_cc_role'] == "admin") ? 'admin' : 'domainadmin'; +$role = "admin"; $is_dual = (!empty($_SESSION["dual-login"]["username"])) ? 'true' : 'false'; $allow_admin_email_login = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["ALLOW_ADMIN_EMAIL_LOGIN"])) ? 'true' : 'false'; diff --git a/data/web/queue.php b/data/web/admin/queue.php similarity index 55% rename from data/web/queue.php rename to data/web/admin/queue.php index ffce8d8b0..85ec59401 100644 --- a/data/web/queue.php +++ b/data/web/admin/queue.php @@ -1,8 +1,17 @@ add('/web/js/site/queue.js'); $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; -$role = ($_SESSION['mailcow_cc_role'] == "admin") ? 'admin' : 'domainadmin'; +$role = "admin"; $template = 'queue.twig'; $template_data = [ diff --git a/data/web/admin.php b/data/web/admin/system.php similarity index 90% rename from data/web/admin.php rename to data/web/admin/system.php index 5a8895dee..c21d43f0d 100644 --- a/data/web/admin.php +++ b/data/web/admin/system.php @@ -1,8 +1,17 @@ thead > tr > th, .table-condensed > tbody > tr > th, .table-condensed > tfoot > tr > th, .table-condensed > thead > tr > td, .table-condensed > tbody > tr > td, .table-condensed > tfoot > tr > td { padding: 3px; } -table tbody tr { - cursor: pointer; -} table tbody tr td input[type="checkbox"] { cursor: pointer; } diff --git a/data/web/domainadmin/index.php b/data/web/domainadmin/index.php new file mode 100644 index 000000000..2d909f97f --- /dev/null +++ b/data/web/domainadmin/index.php @@ -0,0 +1,28 @@ + @$_SESSION['ldelay'], +]; + +$js_minifier->add('/web/js/site/index.js'); +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php'; diff --git a/data/web/domainadmin/mailbox.php b/data/web/domainadmin/mailbox.php new file mode 100644 index 000000000..bb2ef16f3 --- /dev/null +++ b/data/web/domainadmin/mailbox.php @@ -0,0 +1,58 @@ +add('/web/js/site/mailbox.js'); +$js_minifier->add('/web/js/presets/sieveMailbox.js'); +$js_minifier->add('/web/js/site/pwgen.js'); + +$role = "domainadmin"; +$is_dual = (!empty($_SESSION["dual-login"]["username"])) ? 'true' : 'false'; +$allow_admin_email_login = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["ALLOW_ADMIN_EMAIL_LOGIN"])) ? 'true' : 'false'; + +// domains +$domains = mailbox('get', 'domains'); + +// mailboxes +$mailboxes = []; +foreach ($domains as $domain) { + foreach (mailbox('get', 'mailboxes', $domain) as $mailbox) { + $mailboxes[] = $mailbox; + } +} + +$template = 'mailbox.twig'; +$template_data = [ + 'acl' => $_SESSION['acl'], + 'acl_json' => json_encode($_SESSION['acl']), + 'role' => $role, + 'is_dual' => $is_dual, + 'allow_admin_email_login' => $allow_admin_email_login, + 'global_filters' => mailbox('get', 'global_filter_details'), + 'domains' => $domains, + 'mailboxes' => $mailboxes, + 'lang_mailbox' => json_encode($lang['mailbox']), + 'lang_rl' => json_encode($lang['ratelimit']), + 'lang_edit' => json_encode($lang['edit']), + 'lang_datatables' => json_encode($lang['datatables']), +]; + +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php'; \ No newline at end of file diff --git a/data/web/domainadmin/user.php b/data/web/domainadmin/user.php new file mode 100644 index 000000000..7f1b392e0 --- /dev/null +++ b/data/web/domainadmin/user.php @@ -0,0 +1,44 @@ + "get_friendly_names")); + $username = $_SESSION['mailcow_cc_username']; + + $template = 'domainadmin.twig'; + $template_data = [ + 'acl' => $_SESSION['acl'], + 'acl_json' => json_encode($_SESSION['acl']), + 'user_spam_score' => mailbox('get', 'spam_score', $username), + 'tfa_data' => $tfa_data, + 'fido2_data' => $fido2_data, + 'lang_user' => json_encode($lang['user']), + 'lang_datatables' => json_encode($lang['datatables']), + ]; +} +elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') { + header('Location: /admin/dashboard'); + exit(); +} +elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') { + header('Location: /user'); + exit(); +} +else { + header('Location: /domainadmin'); + exit(); +} + +$js_minifier->add('/web/js/site/user.js'); +$js_minifier->add('/web/js/site/pwgen.js'); + +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php'; diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index ed41379f4..233624278 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -3101,6 +3101,7 @@ function clear_session(){ session_write_close(); } function set_user_loggedin_session($user) { + session_regenerate_id(true); $_SESSION['mailcow_cc_username'] = $user; $_SESSION['mailcow_cc_role'] = 'user'; $sogo_sso_pass = file_get_contents("/etc/sogo-sso/sogo-sso.pass"); diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index 7fb619d44..156856c24 100644 --- a/data/web/inc/init_db.inc.php +++ b/data/web/inc/init_db.inc.php @@ -3,7 +3,7 @@ function init_db_schema() { try { global $pdo; - $db_version = "20112024_1105"; + $db_version = "27012025_1555"; $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); diff --git a/data/web/inc/prerequisites.inc.php b/data/web/inc/prerequisites.inc.php index 5738e7c0a..deb5da8fa 100644 --- a/data/web/inc/prerequisites.inc.php +++ b/data/web/inc/prerequisites.inc.php @@ -297,7 +297,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.rspamd.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.tls_policy_maps.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.transports.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/init_db.inc.php'; -require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.inc.php'; +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.global.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/twig.inc.php'; init_db_schema(); if (isset($_SESSION['mailcow_cc_role'])) { diff --git a/data/web/inc/sessions.inc.php b/data/web/inc/sessions.inc.php index 67bdd35b3..bbc08cf13 100644 --- a/data/web/inc/sessions.inc.php +++ b/data/web/inc/sessions.inc.php @@ -99,15 +99,30 @@ if (isset($_POST["logout"])) { unset($_SESSION['sogo-sso-user-allowed']); unset($_SESSION['sogo-sso-pass']); unset($_SESSION["dual-login"]); - header("Location: /mailbox"); + if ($_SESSION["mailcow_cc_role"] == "admin"){ + header("Location: /admin/mailbox"); + } elseif ($_SESSION["mailcow_cc_role"] == "domainadmin") { + header("Location: /domainadmin/mailbox"); + } else { + header("Location: /"); + } exit(); } else { + $role = $_SESSION["mailcow_cc_role"]; session_regenerate_id(true); session_unset(); session_destroy(); session_write_close(); - header("Location: /"); + if ($role == "admin") { + header("Location: /admin"); + } + elseif ($role == "domainadmin") { + header("Location: /domainadmin"); + } + else { + header("Location: /"); + } } } diff --git a/data/web/inc/triggers.admin.inc.php b/data/web/inc/triggers.admin.inc.php new file mode 100644 index 000000000..5b1061f7e --- /dev/null +++ b/data/web/inc/triggers.admin.inc.php @@ -0,0 +1,93 @@ + "admin")); + + if ($as == "admin") { + session_regenerate_id(true); + $_SESSION['mailcow_cc_username'] = $login_user; + $_SESSION['mailcow_cc_role'] = "admin"; + header("Location: /admin/dashboard"); + die(); + } + elseif ($as != "pending") { + unset($_SESSION['pending_mailcow_cc_username']); + unset($_SESSION['pending_mailcow_cc_role']); + unset($_SESSION['pending_tfa_methods']); + unset($_SESSION['mailcow_cc_username']); + unset($_SESSION['mailcow_cc_role']); + } +} + +if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin" && !isset($_SESSION['mailcow_cc_api'])) { + // TODO: Move file upload to API? + if (isset($_POST["submit_main_logo"])) { + if ($_FILES['main_logo']['error'] == 0) { + customize('add', 'main_logo', $_FILES); + } + if ($_FILES['main_logo_dark']['error'] == 0) { + customize('add', 'main_logo_dark', $_FILES); + } + } + if (isset($_POST["reset_main_logo"])) { + customize('delete', 'main_logo'); + customize('delete', 'main_logo_dark'); + } + // Some actions will not be available via API + if (isset($_POST["license_validate_now"])) { + license('verify'); + } + if (isset($_POST["admin_api"])) { + if (isset($_POST["admin_api"]["ro"])) { + admin_api('ro', 'edit', $_POST); + } + elseif (isset($_POST["admin_api"]["rw"])) { + admin_api('rw', 'edit', $_POST); + } + } + if (isset($_POST["admin_api_regen_key"])) { + if (isset($_POST["admin_api_regen_key"]["ro"])) { + admin_api('ro', 'regen_key', $_POST); + } + elseif (isset($_POST["admin_api_regen_key"]["rw"])) { + admin_api('rw', 'regen_key', $_POST); + } + } + if (isset($_POST["rspamd_ui"])) { + rspamd_ui('edit', $_POST); + } + if (isset($_POST["mass_send"])) { + sys_mail($_POST); + } +} +?> diff --git a/data/web/inc/triggers.domainadmin.inc.php b/data/web/inc/triggers.domainadmin.inc.php new file mode 100644 index 000000000..9ee53d67a --- /dev/null +++ b/data/web/inc/triggers.domainadmin.inc.php @@ -0,0 +1,62 @@ + "domain_admin")); + + if ($as == "domainadmin") { + session_regenerate_id(true); + $_SESSION['mailcow_cc_username'] = $login_user; + $_SESSION['mailcow_cc_role'] = "domainadmin"; + header("Location: /domainadmin/mailbox"); + die(); + } + elseif ($as != "pending") { + unset($_SESSION['pending_mailcow_cc_username']); + unset($_SESSION['pending_mailcow_cc_role']); + unset($_SESSION['pending_tfa_methods']); + unset($_SESSION['mailcow_cc_username']); + unset($_SESSION['mailcow_cc_role']); + } +} +?> diff --git a/data/web/inc/triggers.global.inc.php b/data/web/inc/triggers.global.inc.php new file mode 100644 index 000000000..dd88fad56 --- /dev/null +++ b/data/web/inc/triggers.global.inc.php @@ -0,0 +1,48 @@ + "unset_fido2_key", "post_data" => $_POST)); + } +} +?> diff --git a/data/web/inc/triggers.inc.php b/data/web/inc/triggers.inc.php deleted file mode 100644 index b0c2237d6..000000000 --- a/data/web/inc/triggers.inc.php +++ /dev/null @@ -1,263 +0,0 @@ - $_POST['new_password'], - 'new_password2' => $_POST['new_password2'], - 'token' => $_POST['token'], - 'username' => $username, - 'check_tfa' => True - )); - - if ($reset_result){ - header("Location: /"); - exit; - } -} -if (isset($_POST["verify_tfa_login"])) { - if (verify_tfa_login($_SESSION['pending_mailcow_cc_username'], $_POST)) { - if ($_SESSION['pending_mailcow_cc_role'] == "admin") { - $_SESSION['mailcow_cc_username'] = $_SESSION['pending_mailcow_cc_username']; - $_SESSION['mailcow_cc_role'] = "admin"; - unset($_SESSION['pending_mailcow_cc_username']); - unset($_SESSION['pending_mailcow_cc_role']); - unset($_SESSION['pending_tfa_methods']); - - header("Location: /debug"); - die(); - } - elseif ($_SESSION['pending_mailcow_cc_role'] == "domainadmin") { - $_SESSION['mailcow_cc_username'] = $_SESSION['pending_mailcow_cc_username']; - $_SESSION['mailcow_cc_role'] = "domainadmin"; - unset($_SESSION['pending_mailcow_cc_username']); - unset($_SESSION['pending_mailcow_cc_role']); - unset($_SESSION['pending_tfa_methods']); - - header("Location: /mailbox"); - die(); - } - elseif ($_SESSION['pending_mailcow_cc_role'] == "user") { - if (isset($_SESSION['pending_pw_reset_token']) && isset($_SESSION['pending_pw_new_password'])) { - reset_password("reset", array( - 'new_password' => $_SESSION['pending_pw_new_password'], - 'new_password2' => $_SESSION['pending_pw_new_password'], - 'token' => $_SESSION['pending_pw_reset_token'], - 'username' => $_SESSION['pending_mailcow_cc_username'] - )); - unset($_SESSION['pending_pw_reset_token']); - unset($_SESSION['pending_pw_new_password']); - unset($_SESSION['pending_mailcow_cc_username']); - unset($_SESSION['pending_tfa_methods']); - - header("Location: /"); - die(); - } else { - set_user_loggedin_session($_SESSION['pending_mailcow_cc_username']); - $user_details = mailbox("get", "mailbox_details", $_SESSION['mailcow_cc_username']); - $is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false; - if (intval($user_details['attributes']['sogo_access']) == 1 && !$is_dual) { - header("Location: /SOGo/so/{$_SESSION['mailcow_cc_username']}"); - die(); - } else { - header("Location: /user"); - die(); - } - } - } - } - - unset($_SESSION['pending_mailcow_cc_username']); - unset($_SESSION['pending_mailcow_cc_role']); - unset($_SESSION['pending_tfa_methods']); -} - -if (isset($_GET["cancel_tfa_login"])) { - unset($_SESSION['pending_pw_reset_token']); - unset($_SESSION['pending_pw_new_password']); - unset($_SESSION['pending_mailcow_cc_username']); - unset($_SESSION['pending_mailcow_cc_role']); - unset($_SESSION['pending_tfa_methods']); - - header("Location: /"); -} - -if (isset($_POST["quick_release"])) { - quarantine('quick_release', $_POST["quick_release"]); -} - -if (isset($_POST["quick_delete"])) { - quarantine('quick_delete', $_POST["quick_delete"]); -} - -if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) { - $login_user = strtolower(trim($_POST["login_user"])); - $as = check_login($login_user, $_POST["pass_user"]); - - if ($as == "admin") { - $_SESSION['mailcow_cc_username'] = $login_user; - $_SESSION['mailcow_cc_role'] = "admin"; - header("Location: /debug"); - die(); - } - elseif ($as == "domainadmin") { - $_SESSION['mailcow_cc_username'] = $login_user; - $_SESSION['mailcow_cc_role'] = "domainadmin"; - header("Location: /mailbox"); - die(); - } - elseif ($as == "user") { - set_user_loggedin_session($login_user); - $http_parameters = explode('&', $_SESSION['index_query_string']); - unset($_SESSION['index_query_string']); - if (in_array('mobileconfig', $http_parameters)) { - if (in_array('only_email', $http_parameters)) { - header("Location: /mobileconfig.php?only_email"); - die(); - } - header("Location: /mobileconfig.php"); - die(); - } - - $user_details = mailbox("get", "mailbox_details", $login_user); - $is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false; - if (intval($user_details['attributes']['sogo_access']) == 1 && !$is_dual) { - header("Location: /SOGo/so/{$login_user}"); - die(); - } else { - header("Location: /user"); - die(); - } - } - elseif ($as != "pending") { - unset($_SESSION['pending_mailcow_cc_username']); - unset($_SESSION['pending_mailcow_cc_role']); - unset($_SESSION['pending_tfa_methods']); - unset($_SESSION['mailcow_cc_username']); - unset($_SESSION['mailcow_cc_role']); - } -} - -if (isset($_SESSION['mailcow_cc_role']) && (isset($_SESSION['acl']['login_as']) && $_SESSION['acl']['login_as'] == "1")) { - if (isset($_GET["duallogin"])) { - $is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false; - if (!$is_dual) { - $duallogin = html_entity_decode(rawurldecode($_GET["duallogin"])); - if (filter_var($duallogin, FILTER_VALIDATE_EMAIL)) { - if (!empty(mailbox('get', 'mailbox_details', $duallogin))) { - $_SESSION["dual-login"]["username"] = $_SESSION['mailcow_cc_username']; - $_SESSION["dual-login"]["role"] = $_SESSION['mailcow_cc_role']; - $_SESSION['mailcow_cc_username'] = $duallogin; - $_SESSION['mailcow_cc_role'] = "user"; - header("Location: /user"); - } - } - else { - if (!empty(domain_admin('details', $duallogin))) { - $_SESSION["dual-login"]["username"] = $_SESSION['mailcow_cc_username']; - $_SESSION["dual-login"]["role"] = $_SESSION['mailcow_cc_role']; - $_SESSION['mailcow_cc_username'] = $duallogin; - $_SESSION['mailcow_cc_role'] = "domainadmin"; - header("Location: /user"); - } - } - } - } -} - -if (isset($_SESSION['mailcow_cc_role'])) { - if (isset($_POST["set_tfa"])) { - set_tfa($_POST); - } - if (isset($_POST["unset_tfa_key"])) { - unset_tfa_key($_POST); - } - if (isset($_POST["unset_fido2_key"])) { - fido2(array("action" => "unset_fido2_key", "post_data" => $_POST)); - } -} -if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin" && !isset($_SESSION['mailcow_cc_api'])) { - // TODO: Move file upload to API? - if (isset($_POST["submit_main_logo"])) { - if ($_FILES['main_logo']['error'] == 0) { - customize('add', 'main_logo', $_FILES); - } - if ($_FILES['main_logo_dark']['error'] == 0) { - customize('add', 'main_logo_dark', $_FILES); - } - } - if (isset($_POST["reset_main_logo"])) { - customize('delete', 'main_logo'); - customize('delete', 'main_logo_dark'); - } - // Some actions will not be available via API - if (isset($_POST["license_validate_now"])) { - license('verify'); - } - if (isset($_POST["admin_api"])) { - if (isset($_POST["admin_api"]["ro"])) { - admin_api('ro', 'edit', $_POST); - } - elseif (isset($_POST["admin_api"]["rw"])) { - admin_api('rw', 'edit', $_POST); - } - } - if (isset($_POST["admin_api_regen_key"])) { - if (isset($_POST["admin_api_regen_key"]["ro"])) { - admin_api('ro', 'regen_key', $_POST); - } - elseif (isset($_POST["admin_api_regen_key"]["rw"])) { - admin_api('rw', 'regen_key', $_POST); - } - } - if (isset($_POST["rspamd_ui"])) { - rspamd_ui('edit', $_POST); - } - if (isset($_POST["mass_send"])) { - sys_mail($_POST); - } -} -?> diff --git a/data/web/inc/triggers.user.inc.php b/data/web/inc/triggers.user.inc.php new file mode 100644 index 000000000..c16edc10e --- /dev/null +++ b/data/web/inc/triggers.user.inc.php @@ -0,0 +1,132 @@ + $_POST['new_password'], + 'new_password2' => $_POST['new_password2'], + 'token' => $_POST['token'], + 'username' => $username, + 'check_tfa' => True + )); + + if ($reset_result){ + header("Location: /"); + exit; + } +} +if (isset($_POST["verify_tfa_login"])) { + if (verify_tfa_login($_SESSION['pending_mailcow_cc_username'], $_POST)) { + if ($_SESSION['pending_mailcow_cc_role'] == "user") { + if (isset($_SESSION['pending_pw_reset_token']) && isset($_SESSION['pending_pw_new_password'])) { + reset_password("reset", array( + 'new_password' => $_SESSION['pending_pw_new_password'], + 'new_password2' => $_SESSION['pending_pw_new_password'], + 'token' => $_SESSION['pending_pw_reset_token'], + 'username' => $_SESSION['pending_mailcow_cc_username'] + )); + unset($_SESSION['pending_pw_reset_token']); + unset($_SESSION['pending_pw_new_password']); + unset($_SESSION['pending_mailcow_cc_username']); + unset($_SESSION['pending_tfa_methods']); + + header("Location: /"); + die(); + } else { + set_user_loggedin_session($_SESSION['pending_mailcow_cc_username']); + $user_details = mailbox("get", "mailbox_details", $_SESSION['mailcow_cc_username']); + $is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false; + if (intval($user_details['attributes']['sogo_access']) == 1 && !$is_dual) { + header("Location: /SOGo/so/{$_SESSION['mailcow_cc_username']}"); + die(); + } else { + header("Location: /user"); + die(); + } + } + } + } + + unset($_SESSION['pending_mailcow_cc_username']); + unset($_SESSION['pending_mailcow_cc_role']); + unset($_SESSION['pending_tfa_methods']); +} + +if (isset($_GET["cancel_tfa_login"])) { + unset($_SESSION['pending_pw_reset_token']); + unset($_SESSION['pending_pw_new_password']); + unset($_SESSION['pending_mailcow_cc_username']); + unset($_SESSION['pending_mailcow_cc_role']); + unset($_SESSION['pending_tfa_methods']); + + header("Location: /"); +} + +if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) { + $login_user = strtolower(trim($_POST["login_user"])); + $as = check_login($login_user, $_POST["pass_user"], false, array("role" => "user")); + + if ($as == "user") { + set_user_loggedin_session($login_user); + $http_parameters = explode('&', $_SESSION['index_query_string']); + unset($_SESSION['index_query_string']); + if (in_array('mobileconfig', $http_parameters)) { + if (in_array('only_email', $http_parameters)) { + header("Location: /mobileconfig.php?only_email"); + die(); + } + header("Location: /mobileconfig.php"); + die(); + } + + $user_details = mailbox("get", "mailbox_details", $login_user); + $is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false; + if (intval($user_details['attributes']['sogo_access']) == 1 && !$is_dual) { + header("Location: /SOGo/so/{$login_user}"); + die(); + } else { + header("Location: /user"); + die(); + } + } + elseif ($as != "pending") { + unset($_SESSION['pending_mailcow_cc_username']); + unset($_SESSION['pending_mailcow_cc_role']); + unset($_SESSION['pending_tfa_methods']); + unset($_SESSION['mailcow_cc_username']); + unset($_SESSION['mailcow_cc_role']); + } +} +?> diff --git a/data/web/index.php b/data/web/index.php index 0282e4835..1e91cb785 100644 --- a/data/web/index.php +++ b/data/web/index.php @@ -1,5 +1,6 @@ @$_SESSION['oauth2_request'], 'is_mobileconfig' => str_contains($_SESSION['index_query_string'], 'mobileconfig'), diff --git a/data/web/js/site/debug.js b/data/web/js/site/dashboard.js similarity index 100% rename from data/web/js/site/debug.js rename to data/web/js/site/dashboard.js diff --git a/data/web/js/site/mailbox.js b/data/web/js/site/mailbox.js index 0c9ffb3d4..3c21c18b0 100644 --- a/data/web/js/site/mailbox.js +++ b/data/web/js/site/mailbox.js @@ -939,7 +939,7 @@ jQuery(function($){ ' ' + lang.remove + '' + ' Login'; if (ALLOW_ADMIN_EMAIL_LOGIN) { - item.action += ' SOGo'; + item.action += ' SOGo'; } item.action += ''; } diff --git a/data/web/reset-password.php b/data/web/reset-password.php index a0225dc6a..7544d40c3 100644 --- a/data/web/reset-password.php +++ b/data/web/reset-password.php @@ -2,11 +2,11 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') { - header('Location: /debug'); + header('Location: /admin/dashboard'); exit(); } elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { - header('Location: /mailbox'); + header('Location: /domainadmin/mailbox'); exit(); } elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') { diff --git a/data/web/templates/admin_index.twig b/data/web/templates/admin_index.twig new file mode 100644 index 000000000..4127a6a21 --- /dev/null +++ b/data/web/templates/admin_index.twig @@ -0,0 +1,91 @@ +{% extends 'base.twig' %} + +{% block navbar %}{% endblock %} + +{% block content %} +
+
+
+
+ {{ lang.login.login }} +
+ + +
+
+
+ + {% if ui_texts.ui_announcement_text and ui_texts.ui_announcement_active %} +
{{ ui_texts.ui_announcement_text|rot13 }}
+ {% endif %} +
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+
+ + +
+
+
+
{{ lang.login.other_logins }}
+ + {% if login_delay %} +

{{ lang.login.delayed|format(login_delay) }}

+ {% endif %} +
+ {% if (mailcow_apps or app_links) and not hide_mailcow_apps %} + {{ ui_texts.apps_name|raw }}
+
+ {% for app in mailcow_apps %} + {% if not app.hide %} + {% if not skip_sogo or not is_uri('SOGo', app.link) %} + + {% endif %} + {% endif %} + {% endfor %} + {% for row in app_links %} + {% for key, val in row %} + {% if not val.hide %} +
+ {{ key }} +
+ {% endif %} + {% endfor %} + {% endfor %} +
+ {% endif %} +
+
+
+
+{% endblock %} diff --git a/data/web/templates/base.twig b/data/web/templates/base.twig index 2634574d8..4d0a30251 100644 --- a/data/web/templates/base.twig +++ b/data/web/templates/base.twig @@ -66,27 +66,36 @@ + {% endif %} - {% if mailcow_cc_role != 'admin' %} + {% if mailcow_cc_role == 'domainadmin' %} + + {% elseif mailcow_cc_role == 'user' %} {% endif %} - {% if mailcow_cc_role == 'admin' or mailcow_cc_role == 'domainadmin' %} + {% if mailcow_cc_role == 'domainadmin' %} {% endif %} @@ -246,7 +255,7 @@ function recursiveBase64StrToArrayBuffer(obj) { $(".totp-authenticator-selection").click(function(){ $(".totp-authenticator-selection").removeClass("active"); $(this).addClass("active"); - + var id = $(this).children('input').first().val(); $("#totp_selected_id").val(id); @@ -255,7 +264,7 @@ function recursiveBase64StrToArrayBuffer(obj) { if ($('.totp-authenticator-selection').length == 1 && $('#pending_tfa_tab_yubi_otp').length == 0 && $('.webauthn-authenticator-selection').length == 0){ - + // select default if only one authenticator exists $('.totp-authenticator-selection').addClass("active"); @@ -268,7 +277,7 @@ function recursiveBase64StrToArrayBuffer(obj) { $('#pending_tfa_tab_totp').on('shown.bs.tab', function() { // autofocus setTimeout(function() { $("#collapseTotpTFA").find('input[name="token"]').focus(); }, 200); - }); + }); // validate Yubi OTP tfa if ($('.webauthn-authenticator-selection').length == 0){ // autofocus @@ -287,10 +296,10 @@ function recursiveBase64StrToArrayBuffer(obj) { $(".webauthn-authenticator-selection").click(function(){ $(".webauthn-authenticator-selection").removeClass("active"); $(this).addClass("active"); - + var id = $(this).children('input').first().val(); $("#webauthn_selected_id").val(id); - + var webauthn_status_auth = document.getElementById('webauthn_status_auth'); webauthn_status_auth.style.setProperty('display', 'flex', 'important'); var webauthn_return_code = document.getElementById('webauthn_return_code'); @@ -313,7 +322,7 @@ function recursiveBase64StrToArrayBuffer(obj) { console.log(json); if (json.success === false) throw new Error(); if (json.type === "error") throw new Error(json.msg); - + recursiveBase64StrToArrayBuffer(json); return json; }).then(getCredentialArgs => { @@ -340,7 +349,7 @@ function recursiveBase64StrToArrayBuffer(obj) { webauthn_return_code.style.setProperty('display', 'block', 'important'); webauthn_return_code.innerHTML = lang_tfa.error_code + ': ' + err + ' ' + lang_tfa.reload_retry; }); - } + } }); $('#ConfirmTFAModal').on('hidden.bs.modal', function(){ // cancel pending login @@ -551,7 +560,7 @@ function recursiveBase64StrToArrayBuffer(obj) { Version: {{ mailcow_info.version_tag }} - {% endif %} + {% endif %} {% if mailcow_cc_username and mailcow_info.mailcow_branch|lower == "nightly" and mailcow_info.version_tag|default %} 🛠️🐮 + 🐋 = 💕 diff --git a/data/web/templates/debug.twig b/data/web/templates/dashboard.twig similarity index 99% rename from data/web/templates/debug.twig rename to data/web/templates/dashboard.twig index c148856cb..8f8c2d99f 100644 --- a/data/web/templates/debug.twig +++ b/data/web/templates/dashboard.twig @@ -42,8 +42,8 @@ mailcow-logo-dark
-
- +
+
diff --git a/data/web/templates/domainadmin_index.twig b/data/web/templates/domainadmin_index.twig new file mode 100644 index 000000000..4127a6a21 --- /dev/null +++ b/data/web/templates/domainadmin_index.twig @@ -0,0 +1,91 @@ +{% extends 'base.twig' %} + +{% block navbar %}{% endblock %} + +{% block content %} +
+
+
+
+ {{ lang.login.login }} +
+ + +
+
+
+ + {% if ui_texts.ui_announcement_text and ui_texts.ui_announcement_active %} +
{{ ui_texts.ui_announcement_text|rot13 }}
+ {% endif %} +
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+
+ + +
+
+ +
{{ lang.login.other_logins }}
+ + {% if login_delay %} +

{{ lang.login.delayed|format(login_delay) }}

+ {% endif %} +
+ {% if (mailcow_apps or app_links) and not hide_mailcow_apps %} + {{ ui_texts.apps_name|raw }}
+
+ {% for app in mailcow_apps %} + {% if not app.hide %} + {% if not skip_sogo or not is_uri('SOGo', app.link) %} + + {% endif %} + {% endif %} + {% endfor %} + {% for row in app_links %} + {% for key, val in row %} + {% if not val.hide %} +
+ {{ key }} +
+ {% endif %} + {% endfor %} + {% endfor %} +
+ {% endif %} +
+
+
+
+{% endblock %} diff --git a/data/web/templates/user/tab-user-auth.twig b/data/web/templates/user/tab-user-auth.twig index 5c90bedf5..a7e025431 100644 --- a/data/web/templates/user/tab-user-auth.twig +++ b/data/web/templates/user/tab-user-auth.twig @@ -20,11 +20,11 @@ {{ lang.user.open_webmail_sso }} {% elseif dual_login %} - + {{ lang.user.open_webmail_sso }} {% else %} - + {{ lang.user.open_webmail_sso }} {% endif %} diff --git a/data/web/templates/index.twig b/data/web/templates/user_index.twig similarity index 100% rename from data/web/templates/index.twig rename to data/web/templates/user_index.twig diff --git a/data/web/user.php b/data/web/user.php index 19aafdddc..7c34ba953 100644 --- a/data/web/user.php +++ b/data/web/user.php @@ -1,29 +1,8 @@ "get_friendly_names")); - $username = $_SESSION['mailcow_cc_username']; - - $template = 'domainadmin.twig'; - $template_data = [ - 'acl' => $_SESSION['acl'], - 'acl_json' => json_encode($_SESSION['acl']), - 'user_spam_score' => mailbox('get', 'spam_score', $username), - 'tfa_data' => $tfa_data, - 'fido2_data' => $fido2_data, - 'lang_user' => json_encode($lang['user']), - 'lang_datatables' => json_encode($lang['datatables']), - ]; -} -elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') { +if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') { /* / USER @@ -95,6 +74,14 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == ' 'lang_datatables' => json_encode($lang['datatables']), ]; } +elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') { + header('Location: /admin/dashboard'); + exit(); +} +elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { + header('Location: /domainadmin/mailbox'); + exit(); +} else { header('Location: /'); exit(); From f0016eeecdaa74ad28d3e63914895d05f43c8196 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Fri, 7 Feb 2025 14:19:20 +0100 Subject: [PATCH 2/3] [Web] Add german translation for idp settings --- data/web/lang/lang.de-de.json | 37 +++++++++++++++++++ data/web/lang/lang.en-gb.json | 3 ++ .../admin/tab-config-identity-provider.twig | 8 ++-- data/web/templates/admin_index.twig | 2 +- data/web/templates/domainadmin_index.twig | 2 +- data/web/templates/user_index.twig | 2 +- 6 files changed, 47 insertions(+), 7 deletions(-) diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json index 782e4e689..8e164b444 100644 --- a/data/web/lang/lang.de-de.json +++ b/data/web/lang/lang.de-de.json @@ -206,6 +206,39 @@ "help_text": "Hilfstext unter Login-Maske (HTML ist zulässig)", "host": "Host", "html": "HTML", + "iam": "Identity Provider", + "iam_attribute_field": "Attribut Feld", + "iam_authorize_url": "Authorization Endpunkt", + "iam_auth_flow": "Authentication Flow", + "iam_auth_flow_info": "Zusätzlich zum Authorization Code Flow (dem Standard-Flow in Keycloak), der für Single-Sign-On-Logins verwendet wird, unterstützt mailcow auch den Authentication Flow mit direkten Anmeldeinformationen. Der Mailpassword Flow versucht, die Anmeldedaten des Benutzers über die Keycloak Admin REST API zu validieren. Dabei ruft mailcow das gehashte Passwort aus dem mailcow_password Attribut ab, das in Keycloak zugewiesen ist.", + "iam_basedn": "Base DN", + "iam_client_id": "Client ID", + "iam_client_secret": "Client Secret", + "iam_client_scopes": "Client Scopes", + "iam_description": "Konfiguriere einen externen Identity Provider für die Authentifizierung
Die Mailboxen der Benutzer werden bei ihrer ersten Anmeldung automatisch erstellt, vorausgesetzt, dass ein Attribut Mapping festgelegt wurde.", + "iam_extra_permission": "Damit die folgenden Einstellungen funktionieren, benötigt der mailcow Client in Keycloak ein Service-Konto und die Berechtigung view-users.", + "iam_host": "Host", + "iam_host_info": "Gib einen oder mehrere LDAP-Hosts ein, getrennt durch Kommas.", + "iam_import_users": "Import Users", + "iam_mapping": "Attribut Mapping", + "iam_bindpass": "Bind Passwort", + "iam_periodic_full_sync": "Periodic Full Sync", + "iam_port": "Port", + "iam_realm": "Realm", + "iam_redirect_url": "Redirect Url", + "iam_rest_flow": "Mailpassword Flow", + "iam_server_url": "Server Url", + "iam_sso": "Single Sign-On", + "iam_sync_interval": "Sync / Import interval (min)", + "iam_test_connection": "Verbindung Testen", + "iam_token_url": "Token Endpunkt", + "iam_userinfo_url": "User info Endpunkt", + "iam_username_field": "Username Feld", + "iam_binddn": "Bind DN", + "iam_use_ssl": "Benutze SSL", + "iam_use_tls": "Benutze TLS", + "iam_version": "Version", + "ignore_ssl_error": "Ignoriere SSL Errors", "import": "Importieren", "import_private_key": "Private Key importieren", "in_use_by": "Verwendet von", @@ -403,6 +436,7 @@ "goto_empty": "Eine Alias-Adresse muss auf mindestens eine gültige Ziel-Adresse zeigen", "goto_invalid": "Ziel-Adresse %s ist ungültig", "ham_learn_error": "Ham Lernfehler: %s", + "iam_test_connection": "Verbindung fehlgeschlagen", "imagick_exception": "Fataler Bildverarbeitungsfehler", "img_dimensions_exceeded": "Grafik überschreitet die maximale Bildgröße", "img_invalid": "Grafik konnte nicht validiert werden", @@ -766,6 +800,9 @@ "forgot_password": "> Passwort vergessen?", "invalid_pass_reset_token": "Der Rücksetz-Token für das Passwort ist ungültig oder abgelaufen.
Bitte fordern Sie einen neuen Link zur Passwortwiederherstellung an.", "login": "Anmelden", + "login_user": "Benutzer Anmelden", + "login_dadmin": "Domain-Administrator Anmelden", + "login_admin": "Administrator Anmelden", "mobileconfig_info": "Bitte als Mailbox-Benutzer einloggen, um das Verbindungsprofil herunterzuladen.", "new_password": "Neues Passwort", "new_password_confirm": "Neues Passwort bestätigen", diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json index 6d3580e16..362262bff 100644 --- a/data/web/lang/lang.en-gb.json +++ b/data/web/lang/lang.en-gb.json @@ -804,6 +804,9 @@ "forgot_password": "> Forgot Password?", "invalid_pass_reset_token": "The reset password token is invalid or has expired.
Please request a new password reset link.", "login": "Login", + "login_user": "User Login", + "login_dadmin": "Domain-Administrator Login", + "login_admin": "Administrator Login", "mobileconfig_info": "Please login as mailbox user to download the requested Apple connection profile.", "new_password": "New Password", "new_password_confirm": "Confirm new password", diff --git a/data/web/templates/admin/tab-config-identity-provider.twig b/data/web/templates/admin/tab-config-identity-provider.twig index e2cea7fa2..3381fe4d6 100644 --- a/data/web/templates/admin/tab-config-identity-provider.twig +++ b/data/web/templates/admin/tab-config-identity-provider.twig @@ -84,7 +84,7 @@
- Attribute + {{ lang.user.attribute }} {{ lang.mailbox.template }}
@@ -274,8 +274,8 @@
- Attribute - Template + {{ lang.user.attribute }} + {{ lang.mailbox.template }}
@@ -454,7 +454,7 @@
- Attribute + {{ lang.user.attribute }} {{ lang.mailbox.template }}
diff --git a/data/web/templates/admin_index.twig b/data/web/templates/admin_index.twig index 4127a6a21..93a892842 100644 --- a/data/web/templates/admin_index.twig +++ b/data/web/templates/admin_index.twig @@ -7,7 +7,7 @@
- {{ lang.login.login }} + {{ lang.login.login_admin }}
diff --git a/data/web/templates/domainadmin_index.twig b/data/web/templates/domainadmin_index.twig index 4127a6a21..41a9f2597 100644 --- a/data/web/templates/domainadmin_index.twig +++ b/data/web/templates/domainadmin_index.twig @@ -7,7 +7,7 @@
- {{ lang.login.login }} + {{ lang.login.login_dadmin }}
diff --git a/data/web/templates/user_index.twig b/data/web/templates/user_index.twig index 07274e6bd..950482c9a 100644 --- a/data/web/templates/user_index.twig +++ b/data/web/templates/user_index.twig @@ -7,7 +7,7 @@
- {{ lang.login.login }} + {{ lang.login.login_user }}
From 55dcae4a01998b3d9ed861a3f833b6b4df2567e0 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Fri, 7 Feb 2025 15:05:43 +0100 Subject: [PATCH 3/3] [Web] Fix Generic-OIDC connection test --- data/web/inc/functions.inc.php | 30 ++++++++++++++++++++++++------ data/web/lang/lang.de-de.json | 1 + 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index a434582c5..7969c6bb4 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -2337,12 +2337,7 @@ function identity_provider($_action = null, $_data = null, $_extra = null) { switch ($_data['authsource']) { case 'keycloak': - case 'generic-oidc': - if ($_data['authsource'] == 'keycloak') { - $url = "{$_data['server_url']}/realms/{$_data['realm']}/protocol/openid-connect/token"; - } else { - $url = $_data['token_url']; - } + $url = "{$_data['server_url']}/realms/{$_data['realm']}/protocol/openid-connect/token"; $req = http_build_query(array( 'grant_type' => 'client_credentials', 'client_id' => $_data['client_id'], @@ -2355,6 +2350,29 @@ function identity_provider($_action = null, $_data = null, $_extra = null) { curl_setopt($curl, CURLOPT_POSTFIELDS, $req); curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: application/x-www-form-urlencoded')); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + if ($_data['ignore_ssl_error'] == "1"){ + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); + } + $res = curl_exec($curl); + $code = curl_getinfo($curl, CURLINFO_HTTP_CODE); + curl_close ($curl); + + if ($code != 200) { + return false; + } + break; + case 'generic-oidc': + $url = $_data['token_url']; + $curl = curl_init(); + curl_setopt($curl, CURLOPT_URL, $url); + curl_setopt($curl, CURLOPT_TIMEOUT, 7); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "OPTIONS"); + if ($_data['ignore_ssl_error'] == "1"){ + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); + } $res = curl_exec($curl); $code = curl_getinfo($curl, CURLINFO_HTTP_CODE); curl_close ($curl); diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json index 8e164b444..9c54d3fc8 100644 --- a/data/web/lang/lang.de-de.json +++ b/data/web/lang/lang.de-de.json @@ -1114,6 +1114,7 @@ "forwarding_host_removed": "Weiterleitungs-Host %s wurde entfernt", "global_filter_written": "Filterdatei wurde erfolgreich geschrieben", "hash_deleted": "Hash wurde gelöscht", + "iam_test_connection": "Verbindung erfolgreich", "ip_check_opt_in_modified": "IP Check wurde erfolgreich gespeichert", "item_deleted": "Objekt %s wurde entfernt", "item_released": "Objekt %s freigegeben",
Hostname