From 2d55b54904c5111180910e95620445a931f26710 Mon Sep 17 00:00:00 2001 From: andryyy Date: Fri, 4 Jun 2021 14:29:39 +0200 Subject: [PATCH] [Web] Show users the last known connections for SASL authentication [Web] Feature: Log SASL authentication --- .../css/build/006-footable.bootstrap.min.css | 4 +- data/web/css/site/user.css | 8 ++ data/web/debug.php | 18 ++++ data/web/inc/functions.inc.php | 84 ++++++++++++++++--- data/web/inc/functions.mailbox.inc.php | 48 +++++++---- data/web/inc/init_db.inc.php | 26 +++++- data/web/inc/triggers.inc.php | 3 - data/web/js/site/debug.js | 54 +++++++++++- data/web/js/site/user.js | 65 +++++++++++++- data/web/json_api.php | 28 ++++++- data/web/lang/lang.de.json | 12 +++ data/web/lang/lang.en.json | 14 +++- data/web/user.php | 24 ++---- 13 files changed, 329 insertions(+), 59 deletions(-) diff --git a/data/web/css/build/006-footable.bootstrap.min.css b/data/web/css/build/006-footable.bootstrap.min.css index 670eab25..435054f6 100644 --- a/data/web/css/build/006-footable.bootstrap.min.css +++ b/data/web/css/build/006-footable.bootstrap.min.css @@ -132,7 +132,7 @@ table.footable > tbody > tr.footable-empty > th { content: "\f52a"; } .fooicon-remove:before { - content: "\f64f"; + content: "\f62a"; } .fooicon-sort:before { content: "\f3c6"; @@ -147,7 +147,7 @@ table.footable > tbody > tr.footable-empty > th { content: "\f4c9"; } .fooicon-trash:before { - content: "\f64f"; + content: "\f62a"; } .fooicon-eye-close:before { content: "\f33f"; diff --git a/data/web/css/site/user.css b/data/web/css/site/user.css index 732d9c1b..b1ade575 100644 --- a/data/web/css/site/user.css +++ b/data/web/css/site/user.css @@ -111,4 +111,12 @@ border-bottom-width: 3px; padding: .1em .5em .1em; font-size: inherit; font-weight: 400; +} +.clear-last-logins { + cursor: pointer; + margin-top: 10px; + font-size:90%; + font-style: italic; + color: #158cba; + user-select:none; } \ No newline at end of file diff --git a/data/web/debug.php b/data/web/debug.php index a935ea20..26daccf0 100644 --- a/data/web/debug.php +++ b/data/web/debug.php @@ -29,6 +29,7 @@ $xmpp_status = xmpp_control('status');
  • Rspamd
  • mailcow UI
  • +
  • SASL
  • @@ -217,6 +218,23 @@ $xmpp_status = xmpp_control('status'); +
    +
    +
    SASL +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    Dovecot diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 6a451cd9..649aa4e7 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -251,20 +251,60 @@ function password_check($password1, $password2) { return true; } -function last_login($user) { +function last_login($action, $username) { global $pdo; - $stmt = $pdo->prepare('SELECT `remote`, `time` FROM `logs` - WHERE JSON_EXTRACT(`call`, "$[0]") = "check_login" - AND JSON_EXTRACT(`call`, "$[1]") = :user - AND `type` = "success" ORDER BY `time` DESC LIMIT 1'); - $stmt->execute(array(':user' => $user)); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - if (!empty($row)) { - return $row; - } - else { - return false; + switch ($action) { + case 'get': + if (filter_var($username, FILTER_VALIDATE_EMAIL) && hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) { + $stmt = $pdo->prepare('SELECT `real_rip`, MAX(`datetime`) as `datetime`, `service` FROM `sasl_logs` + WHERE `username` = :username + AND `success` = 1 + GROUP BY `real_rip`, `service` + ORDER BY `datetime` DESC + LIMIT 5;'); + $stmt->execute(array(':username' => $username)); + $sasl = $stmt->fetchAll(PDO::FETCH_ASSOC); + foreach ($sasl as $k => $v) { + if (!filter_var($sasl[$k]['real_rip'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { + $sasl[$k]['real_rip'] = 'Web/EAS/Internal (' . $sasl[$k]['real_rip'] . ')'; + } + } + } + else { + $sasl = array(); + } + if ($_SESSION['mailcow_cc_role'] == "admin" || $username == $_SESSION['mailcow_cc_username']) { + $stmt = $pdo->prepare('SELECT `remote`, `time` FROM `logs` + WHERE JSON_EXTRACT(`call`, "$[0]") = "check_login" + AND JSON_EXTRACT(`call`, "$[1]") = :username + AND `type` = "success" ORDER BY `time` DESC LIMIT 1 OFFSET 1'); + $stmt->execute(array(':username' => $username)); + $ui = $stmt->fetch(PDO::FETCH_ASSOC); + } + else { + $ui = array(); + } + + return array('ui' => $ui, 'sasl' => $sasl); + break; + case 'reset': + if (filter_var($username, FILTER_VALIDATE_EMAIL) && hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) { + $stmt = $pdo->prepare('DELETE FROM `sasl_logs` + WHERE `username` = :username + AND `success` = 1;'); + $stmt->execute(array(':username' => $username)); + } + if ($_SESSION['mailcow_cc_role'] == "admin" || $username == $_SESSION['mailcow_cc_username']) { + $stmt = $pdo->prepare('DELETE FROM `logs` + WHERE JSON_EXTRACT(`call`, "$[0]") = "check_login" + AND JSON_EXTRACT(`call`, "$[1]") = :username + AND `type` = "success"'); + $stmt->execute(array(':username' => $username)); + } + return true; + break; } + } function flush_memcached() { try { @@ -1862,6 +1902,26 @@ function get_logs($application, $lines = false) { return $data; } } + if ($application == "sasl") { + if (isset($from) && isset($to)) { + $stmt = $pdo->prepare("SELECT * FROM `sasl_logs` ORDER BY `id` DESC LIMIT :from, :to"); + $stmt->execute(array( + ':from' => $from - 1, + ':to' => $to + )); + $data = $stmt->fetchAll(PDO::FETCH_ASSOC); + } + else { + $stmt = $pdo->prepare("SELECT * FROM `sasl_logs` ORDER BY `id` DESC LIMIT :lines"); + $stmt->execute(array( + ':lines' => $lines + 1, + )); + $data = $stmt->fetchAll(PDO::FETCH_ASSOC); + } + if (is_array($data)) { + return $data; + } + } // Redis if ($application == "dovecot-mailcow") { if (isset($from) && isset($to)) { diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 8bea647b..12e78047 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -3503,18 +3503,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { return false; } $mailboxdata = array(); - $last_imap_login = $redis->Get('last-login/imap/' . $_data); - $last_smtp_login = $redis->Get('last-login/smtp/' . $_data); - $last_pop3_login = $redis->Get('last-login/pop3/' . $_data); - if ($last_imap_login === false || $GLOBALS['SHOW_LAST_LOGIN'] === false) { - $last_imap_login = '0'; - } - if ($last_smtp_login === false || $GLOBALS['SHOW_LAST_LOGIN'] === false) { - $last_smtp_login = '0'; - } - if ($last_pop3_login === false || $GLOBALS['SHOW_LAST_LOGIN'] === false) { - $last_pop3_login = '0'; - } if (preg_match('/y|yes/i', getenv('MASTER'))) { $stmt = $pdo->prepare("SELECT `domain`.`backupmx`, @@ -3575,10 +3563,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $mailboxdata['quota_used'] = intval($row['bytes']); $mailboxdata['percent_in_use'] = ($row['quota'] == 0) ? '- ' : round((intval($row['bytes']) / intval($row['quota'])) * 100); - $mailboxdata['last_imap_login'] = $last_imap_login; - $mailboxdata['last_smtp_login'] = $last_smtp_login; - $mailboxdata['last_pop3_login'] = $last_pop3_login; - if ($mailboxdata['percent_in_use'] === '- ') { $mailboxdata['percent_class'] = "info"; } @@ -3592,11 +3576,43 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $mailboxdata['percent_class'] = "success"; } + // Determine last logins + $stmt = $pdo->prepare("SELECT MAX(`datetime`) AS `datetime`, `service` FROM `sasl_logs` + WHERE `username` = :mailbox + AND `success` = 1 + GROUP BY `service` DESC"); + $stmt->execute(array(':mailbox' => $_data)); + $SaslLogsData = $stmt->fetchAll(PDO::FETCH_ASSOC); + foreach ($SaslLogsData as $SaslLogs) { + if ($SaslLogs['service'] == 'imap') { + $last_imap_login = strtotime($SaslLogs['datetime']); + } + else if ($SaslLogs['service'] == 'smtp') { + $last_smtp_login = strtotime($SaslLogs['datetime']); + } + else if ($SaslLogs['service'] == 'pop3') { + $last_pop3_login = strtotime($SaslLogs['datetime']); + } + } + if (!isset($last_imap_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) { + $last_imap_login = 0; + } + if (!isset($last_smtp_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) { + $last_smtp_login = 0; + } + if (!isset($last_pop3_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) { + $last_pop3_login = 0; + } + $mailboxdata['last_imap_login'] = $last_imap_login; + $mailboxdata['last_smtp_login'] = $last_smtp_login; + $mailboxdata['last_pop3_login'] = $last_pop3_login; + if (!isset($_extra) || $_extra != 'reduced') { $rl = ratelimit('get', 'mailbox', $_data); $stmt = $pdo->prepare("SELECT `maxquota`, `quota` FROM `domain` WHERE `domain` = :domain"); $stmt->execute(array(':domain' => $row['domain'])); $DomainQuota = $stmt->fetch(PDO::FETCH_ASSOC); + $stmt = $pdo->prepare("SELECT IFNULL(COUNT(`active`), 0) AS `pushover_active` FROM `pushover` WHERE `username` = :username AND `active` = 1"); $stmt->execute(array(':username' => $_data)); $PushoverActive = $stmt->fetch(PDO::FETCH_ASSOC); diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index 797ca422..c349593c 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 = "27052021_2000"; + $db_version = "03062021_2320"; $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); @@ -510,6 +510,30 @@ function init_db_schema() { ), "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" ), + "sasl_logs" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "success" => "TINYINT(1) NOT NULL DEFAULT '0'", + "service" => "VARCHAR(32) NOT NULL DEFAULT ''", + "app_password" => "INT", + "username" => "VARCHAR(255) NOT NULL", + "real_rip" => "VARCHAR(64) NOT NULL", + "datetime" => "DATETIME(0) NOT NULL DEFAULT NOW(0)" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ), + "key" => array( + "username" => array("username"), + "service" => array("service"), + "success" => array("success"), + "datetime" => array("datetime"), + "real_rip" => array("real_rip") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), "quota2" => array( "cols" => array( "username" => "VARCHAR(255) NOT NULL", diff --git a/data/web/inc/triggers.inc.php b/data/web/inc/triggers.inc.php index 478bb4bf..4389ab35 100644 --- a/data/web/inc/triggers.inc.php +++ b/data/web/inc/triggers.inc.php @@ -24,19 +24,16 @@ if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) { if ($as == "admin") { $_SESSION['mailcow_cc_username'] = $login_user; $_SESSION['mailcow_cc_role'] = "admin"; - $_SESSION['mailcow_cc_last_login'] = last_login($login_user); header("Location: /admin"); } elseif ($as == "domainadmin") { $_SESSION['mailcow_cc_username'] = $login_user; $_SESSION['mailcow_cc_role'] = "domainadmin"; - $_SESSION['mailcow_cc_last_login'] = last_login($login_user); header("Location: /mailbox"); } elseif ($as == "user") { $_SESSION['mailcow_cc_username'] = $login_user; $_SESSION['mailcow_cc_role'] = "user"; - $_SESSION['mailcow_cc_last_login'] = last_login($login_user); $http_parameters = explode('&', $_SESSION['index_query_string']); unset($_SESSION['index_query_string']); if (in_array('mobileconfig', $http_parameters)) { diff --git a/data/web/js/site/debug.js b/data/web/js/site/debug.js index fa9ffbb5..866dba2c 100644 --- a/data/web/js/site/debug.js +++ b/data/web/js/site/debug.js @@ -271,7 +271,7 @@ jQuery(function($){ {"name":"role","title":"Role"}, {"name":"remote","title":"IP"}, {"name":"msg","title":lang.message,"style":{"word-break":"break-all"}}, - {"name":"call","title":"Call","breakpoints": "all"}, + {"name":"call","title":"Call","breakpoints": "all"} ], "rows": $.ajax({ dataType: 'json', @@ -301,6 +301,43 @@ jQuery(function($){ } }); } + function draw_sasl_logs() { + ft_api_logs = FooTable.init('#sasl_logs', { + "columns": [ + {"name":"success","title":lang.success,"filterable": false,"style":{"width":"30px"}}, + {"name":"username","title":lang.username}, + {"name":"service","title":lang.service}, + {"name":"real_rip","title":"IP"}, + {"sorted": true,"sortValue": function(value){res = new Date(value);return res.getTime();},"direction":"DESC","name":"datetime","formatter":function date_format(datetime) { var date = new Date(datetime.replace(/-/g, "/")); return date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});},"title":lang.login_time,"style":{"width":"170px"}}, + ], + "rows": $.ajax({ + dataType: 'json', + url: '/api/v1/get/logs/sasl', + jsonp: false, + error: function () { + console.log('Cannot draw sasl log table'); + }, + success: function (data) { + return process_table_data(data, 'sasl_log_table'); + } + }), + "empty": lang.empty, + "paging": {"enabled": true,"limit": 5,"size": log_pagination_size}, + "filtering": {"enabled": true,"delay": 1200,"position": "left","connectors": false,"placeholder": lang.filter_table,"connectors": false}, + "sorting": {"enabled": true}, + "on": { + "destroy.ft.table": function(e, ft){ + $('.refresh_table').attr('disabled', 'true'); + }, + "ready.ft.table": function(e, ft){ + table_log_ready(ft, 'sasl_logs'); + }, + "after.ft.paging": function(e, ft){ + table_log_paging(ft, 'sasl_logs'); + } + } + }); + } function draw_acme_logs() { ft_acme_logs = FooTable.init('#acme_log', { "columns": [ @@ -666,6 +703,20 @@ jQuery(function($){ item.task = '' + item.task + ''; item.type = '' + item.type + ''; }); + } else if (table == 'sasl_log_table') { + $.each(data, function (i, item) { + if (item === null) { return true; } + item.username = escapeHtml(item.username); + if (item.service == "smtp") { item.service = '
    ' + item.service.toUpperCase() + '
    '; } + else if (item.service == "imap") { item.service = '
    ' + item.service.toUpperCase() + '
    '; } + else { item.service = '
    ' + item.service.toUpperCase() + '
    '; } + if (item.success == 0) { + item.success = ''; + } + else { + item.success = ''; + } + }); } else if (table == 'general_syslog') { $.each(data, function (i, item) { if (item === null) { return true; } @@ -750,6 +801,7 @@ jQuery(function($){ draw_api_logs(); draw_rl_logs(); draw_ui_logs(); + draw_sasl_logs(); draw_netfilter_logs(); draw_rspamd_history(); $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { diff --git a/data/web/js/site/user.js b/data/web/js/site/user.js index c2ff3897..0ccabcf4 100644 --- a/data/web/js/site/user.js +++ b/data/web/js/site/user.js @@ -42,6 +42,7 @@ $(document).ready(function() { }); $(".arrow-toggle").on('click', function(e) { e.preventDefault(); $(this).find('.arrow').toggleClass("animation"); }); $("#pushover_delete").click(function() { return confirm(lang.delete_ays); }); + }); jQuery(function($){ // http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery @@ -70,8 +71,65 @@ jQuery(function($){ return date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"}); } acl_data = JSON.parse(acl); - var last_login = $('.last_login_date').data('time'); - $('.last_login_date').text(unix_time_format(last_login)); + + $('.clear-last-logins').on('click', function () { + if (confirm(lang.delete_ays)) { + last_logins('reset'); + } + }) + + function last_logins(action) { + if (action == 'get') { + $.ajax({ + dataType: 'json', + url: '/api/v1/get/last-login/' + encodeURIComponent(mailcow_cc_username), + jsonp: false, + error: function () { + console.log('error reading last logins'); + }, + success: function (data) { + $('.last-login').html(); + if (data.ui.time) { + $('.last-login').html(' ' + lang.last_ui_login + ': ' + unix_time_format(data.ui.time)); + } else { + $('.last-login').text(lang.no_last_login); + } + if (data.sasl) { + $('.last-login').append('
      '); + $.each(data.sasl, function (i, item) { + var datetime = new Date(item.datetime.replace(/-/g, "/")); + var local_datetime = datetime.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"}); + if (item.service == "smtp") { service = '
      ' + item.service.toUpperCase() + '
      '; } + else if (item.service == "imap") { service = '
      ' + item.service.toUpperCase() + '
      '; } + else { service = '
      ' + item.service.toUpperCase() + '
      '; } + if (item.real_rip.startsWith("Web")) { + real_rip = item.real_rip; + } else { + real_rip = '' + item.real_rip + ''; + } + $('.last-login').append('
    • ' + + local_datetime + ' ' + service + ' ' + lang.from + ' ' + + real_rip + + '
    • '); + }) + $('.last-login').append('
    '); + } + } + }) + } else if (action == 'reset') { + $.ajax({ + dataType: 'json', + url: '/api/v1/get/reset-last-login/' + encodeURIComponent(mailcow_cc_username), + jsonp: false, + error: function () { + console.log('cannot reset last logins'); + }, + success: function (data) { + last_logins('get'); + } + }) + } + } function draw_tla_table() { ft_tla_table = FooTable.init('#tla_table', { @@ -132,7 +190,7 @@ jQuery(function($){ {"name":"log","title":"Log"}, {"name":"active","filterable": false,"style":{"maxWidth":"70px","width":"70px"},"title":lang.active,"formatter": function(value){return 1==value?'':0==value&&'';}}, {"name":"is_running","filterable": false,"style":{"maxWidth":"120px","width":"100px"},"title":lang.status}, - {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"240px","width":"240px"},"type":"html","title":lang.action,"breakpoints":"xs sm"} + {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","min-width":"260px","width":"260px"},"type":"html","title":lang.action,"breakpoints":"xs sm"} ], "empty": lang.empty, "rows": $.ajax({ @@ -324,6 +382,7 @@ jQuery(function($){ draw_tla_table(); draw_wl_policy_mailbox_table(); draw_bl_policy_mailbox_table(); + last_logins('get'); // FIDO2 friendly name modal $('#fido2ChangeFn').on('show.bs.modal', function (e) { diff --git a/data/web/json_api.php b/data/web/json_api.php index a1198f2f..e8e9e888 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -306,7 +306,6 @@ if (isset($_GET['query'])) { $_SESSION["mailcow_cc_role"] = "domainadmin"; } $_SESSION["mailcow_cc_username"] = $process_fido2['username']; - $_SESSION['mailcow_cc_last_login'] = last_login($process_fido2['username']); $_SESSION["fido2_cid"] = $process_fido2['cid']; unset($_SESSION["challenge"]); $_SESSION['return'][] = array( @@ -640,6 +639,21 @@ if (isset($_GET['query'])) { } break; + case "last-login": + if ($object) { + $data = last_login('get', $object); + process_get_return($data); + } + break; + + // Todo: move to delete + case "reset-last-login": + if ($object) { + $data = last_login('reset', $object); + process_get_return($data); + } + break; + case "transport": switch ($object) { case "all": @@ -800,6 +814,17 @@ if (isset($_GET['query'])) { } echo (isset($logs) && !empty($logs)) ? json_encode($logs, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) : '{}'; break; + case "sasl": + // 0 is first record, so empty is fine + if (isset($extra)) { + $extra = preg_replace('/[^\d\-]/i', '', $extra); + $logs = get_logs('sasl', $extra); + } + else { + $logs = get_logs('sasl'); + } + echo (isset($logs) && !empty($logs)) ? json_encode($logs, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) : '{}'; + break; case "watchdog": // 0 is first record, so empty is fine if (isset($extra)) { @@ -1458,7 +1483,6 @@ if (isset($_GET['query'])) { process_delete_return(dkim('delete', array('domains' => $items))); break; case "domain": - file_put_contents('/tmp/dssaa', $items); process_delete_return(mailbox('delete', 'domain', array('domain' => $items))); break; case "alias-domain": diff --git a/data/web/lang/lang.de.json b/data/web/lang/lang.de.json index 494cf1fa..a35ecf82 100644 --- a/data/web/lang/lang.de.json +++ b/data/web/lang/lang.de.json @@ -119,6 +119,10 @@ "validation_success": "Erfolgreich validiert" }, "admin": { + "success": "Erfolg", + "service": "Dienst", + "login_time": "Zeit", + "username": "Benutzername", "access": "Zugang", "action": "Aktion", "activate_api": "API aktivieren", @@ -489,6 +493,10 @@ "started_at": "Gestartet am", "solr_status": "Solr Status", "uptime": "Uptime", + "success": "Erfolg", + "service": "Dienst", + "login_time": "Zeit", + "username": "Benutzername", "started_on": "Gestartet am", "static_logs": "Statische Logs", "system_containers": "System & Container", @@ -1032,6 +1040,9 @@ "excludes": "Ausschlüsse", "expire_in": "Ungültig in", "force_pw_update": "Das Passwort für diesen Benutzer muss geändert werden, damit die Zugriffssperre auf die Groupware-Komponenten wieder freigeschaltet wird.", + "from": "von", + "recent_successful_connections": "Kürzlich erfolgreiche Verbindungen", + "clear_recent_successful_connections": "Alle erfolgreichen Verbindungen bereinigen", "generate": "generieren", "hour": "Stunde", "hourly": "Stündlich", @@ -1041,6 +1052,7 @@ "is_catch_all": "Ist Catch-All-Adresse für Domain(s)", "last_mail_login": "Letzter Mail-Login", "last_run": "Letzte Ausführung", + "last_ui_login": "Letzte UI Anmeldung", "loading": "Lade...", "mailbox_details": "Mailbox-Details", "messages": "Nachrichten", diff --git a/data/web/lang/lang.en.json b/data/web/lang/lang.en.json index 932eb34d..fc015299 100644 --- a/data/web/lang/lang.en.json +++ b/data/web/lang/lang.en.json @@ -117,6 +117,10 @@ "validation_success": "Validated successfully" }, "admin": { + "success": "Success", + "service": "Service", + "login_time": "Login time", + "username": "Username", "access": "Access", "action": "Action", "activate_api": "Activate API", @@ -486,8 +490,12 @@ "size": "Size", "started_at": "Started at", "solr_status": "Solr status", - "uptime": "Uptime", "started_on": "Started on", + "uptime": "Uptime", + "success": "Success", + "service": "Service", + "login_time": "Time", + "username": "Username", "static_logs": "Static logs", "system_containers": "System & Containers", "xmpp_status": "XMPP status" @@ -1030,6 +1038,9 @@ "excludes": "Excludes", "expire_in": "Expire in", "force_pw_update": "You must set a new password to be able to access groupware related services.", + "from": "from", + "recent_successful_connections": "Seen successful connections", + "clear_recent_successful_connections": "Clear seen successful connections", "generate": "generate", "hour": "hour", "hourly": "Hourly", @@ -1039,6 +1050,7 @@ "is_catch_all": "Catch-all for domain/s", "last_mail_login": "Last mail login", "last_run": "Last run", + "last_ui_login": "Last UI login", "loading": "Loading...", "mailbox_details": "Mailbox details", "messages": "messages", diff --git a/data/web/user.php b/data/web/user.php index 61a04be2..466ffc4d 100644 --- a/data/web/user.php +++ b/data/web/user.php @@ -22,15 +22,8 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'doma

    []

    -

    - - () - -

    + +

    @@ -181,15 +174,10 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '

    []

    []

    []

    -

    - - () - -

    +
    +

    + +