public
nobgit
read
NobMail
Based on mailcow: dockerized
Languages
Repository composition by tracked source files.
PHP
49%
JavaScript
35%
HTML
9%
CSS
4%
Shell
2%
Python
1%
Lua
0%
Perl
0%
Ruby
0%
SCSS
0%
Create file
Wiki Documentation
Clone
https://nobgit.com/orgs/nobgit/nobmail.git
ssh://[email protected]:2222/orgs/nobgit/nobmail.git
Commit
[Web] Show users the last known connections for SASL authentication [Web] Feature: Log SASL authentication
2d55b549
data/web/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
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');
<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>
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 = '<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) {
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('<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) {
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 <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",
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 <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",
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
<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>