[Web] Show users the last known connections for SASL authentication

[Web] Feature: Log SASL authentication
master
andryyy 2021-06-04 14:29:39 +02:00
parent 51e3521aac
commit 2d55b54904
No known key found for this signature in database
GPG Key ID: 8EC34FF2794E25EF
13 changed files with 329 additions and 59 deletions

View File

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

View File

@ -112,3 +112,11 @@ border-bottom-width: 3px;
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;
}

View File

@ -29,6 +29,7 @@ $xmpp_status = xmpp_control('status');
<li role="presentation"><a href="#tab-rspamd-history" aria-controls="tab-rspamd-history" role="tab" data-toggle="tab">Rspamd</a></li>
<li role="presentation"><span class="dropdown-desc"><?=$lang['debug']['static_logs'];?></span></li>
<li role="presentation"><a href="#tab-ui" aria-controls="tab-ui" role="tab" data-toggle="tab">mailcow UI</a></li>
<li role="presentation"><a href="#tab-sasl" aria-controls="tab-sasl" role="tab" data-toggle="tab">SASL</a></li>
</ul>
</li>
</ul>
@ -217,6 +218,23 @@ $xmpp_status = xmpp_control('status');
</div>
</div>
<div role="tabpanel" class="tab-pane" id="tab-sasl">
<div class="panel panel-default">
<div class="panel-heading">SASL <span class="badge badge-info table-lines"></span>
<div class="btn-group pull-right">
<button class="btn btn-xs btn-default add_log_lines" data-post-process="sasl_log_table" data-table="sasl_logs" data-log-url="ui" data-nrows="1000">+ 1000</button>
<button class="btn btn-xs btn-default add_log_lines" data-post-process="sasl_log_table" data-table="sasl_logs" data-log-url="ui" data-nrows="10000">+ 10000</button>
<button class="btn btn-xs btn-default refresh_table" data-draw="draw_sasl_logs" data-table="sasl_logs"><?=$lang['admin']['refresh'];?></button>
</div>
</div>
<div class="panel-body">
<div class="table-responsive">
<table class="table table-striped table-condensed" id="sasl_logs"></table>
</div>
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="tab-dovecot-logs">
<div class="panel panel-default">
<div class="panel-heading">Dovecot <span class="badge badge-info table-lines"></span>

View File

@ -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;
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 {
return false;
$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)) {

View File

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

View File

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

View File

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

View File

@ -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 = '<code>' + item.task + '</code>';
item.type = '<span class="label label-' + item.type + '">' + item.type + '</span>';
});
} 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 = '<div class="label label-default">' + item.service.toUpperCase() + '<i class="bi bi-chevron-compact-right"></i></div>'; }
else if (item.service == "imap") { item.service = '<div class="label label-default"><i class="bi bi-chevron-compact-left"></i> ' + item.service.toUpperCase() + '</div>'; }
else { item.service = '<div class="label label-default">' + item.service.toUpperCase() + '</div>'; }
if (item.success == 0) {
item.success = '<span class="label label-danger"><i class="bi bi-person-x-fill"></i></span>';
}
else {
item.success = '<span class="label label-success"><i class="bi bi-person-check-fill"></i></span>';
}
});
} 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) {

View File

@ -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('<i class="bi bi-person-fill"></i> ' + lang.last_ui_login + ': ' + unix_time_format(data.ui.time));
} else {
$('.last-login').text(lang.no_last_login);
}
if (data.sasl) {
$('.last-login').append('<ul class="list-group">');
$.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 = '<div class="label label-default">' + item.service.toUpperCase() + '<i class="bi bi-chevron-compact-right"></i></div>'; }
else if (item.service == "imap") { service = '<div class="label label-default"><i class="bi bi-chevron-compact-left"></i> ' + item.service.toUpperCase() + '</div>'; }
else { service = '<div class="label label-default">' + item.service.toUpperCase() + '</div>'; }
if (item.real_rip.startsWith("Web")) {
real_rip = item.real_rip;
} else {
real_rip = '<a href="https://bgp.he.net/ip/' + item.real_rip + '" target="_blank">' + item.real_rip + '</a>';
}
$('.last-login').append('<li class="list-group-item">' +
local_datetime + ' ' + service + ' ' + lang.from + ' ' +
real_rip +
'</li>');
})
$('.last-login').append('</ul>');
}
}
})
} 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?'<i class="bi bi-check-lg"></i>':0==value&&'<i class="bi bi-x-lg"></i>';}},
{"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) {

View File

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

View File

@ -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 <b>muss</b> 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",

View File

@ -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 <b>must</b> 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",

View File

@ -22,15 +22,8 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'doma
<div class="row">
<div class="col-sm-offset-3 col-sm-9">
<p><a href="#pwChangeModal" data-toggle="modal">[<?=$lang['user']['change_password'];?>]</a></p>
<p>
<?php
if ($_SESSION['mailcow_cc_last_login']['remote']):
?>
<i class="bi bi-person-bounding-box"></i> <span data-time="<?=$_SESSION['mailcow_cc_last_login']['time'];?>" class="last_login_date"></span> (<?=$_SESSION['mailcow_cc_last_login']['remote'];?>)
<?php
else: echo $lang['user']['no_last_login']; endif;
?>
</p>
<div class="last-login"></div>
<div class="clear-last-logins"><?=$lang['user']['clear_recent_successful_connections'];?></div>
<p>
</div>
</div>
@ -181,15 +174,10 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
<p><a href="#pwChangeModal" data-toggle="modal">[<?=$lang['user']['change_password'];?>]</a></p>
<p><a target="_blank" href="https://mailcow.github.io/mailcow-dockerized-docs/client/#<?=$clientconfigstr;?>">[<?=$lang['user']['client_configuration'];?>]</a></p>
<p><a href="#userFilterModal" data-toggle="modal">[<?=$lang['user']['show_sieve_filters'];?>]</a></p>
<p>
<?php
if ($_SESSION['mailcow_cc_last_login']['remote']):
?>
<i class="bi bi-person-bounding-box"></i> <span data-time="<?=$_SESSION['mailcow_cc_last_login']['time'];?>" class="last_login_date"></span> (<?=$_SESSION['mailcow_cc_last_login']['remote'];?>)
<?php
else: echo $lang['user']['no_last_login']; endif;
?>
</p>
<hr>
<h4><?=$lang['user']['recent_successful_connections'];?></h4>
<div class="last-login"></div>
<div class="clear-last-logins"><?=$lang['user']['clear_recent_successful_connections'];?></div>
</div>
</div>
<hr>