Browse Source

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

andryyy 4 years ago
parent
commit
2d55b54904

+ 2 - 2
data/web/css/build/006-footable.bootstrap.min.css

@@ -132,7 +132,7 @@ table.footable > tbody > tr.footable-empty > th {
   content: "\f52a";
   content: "\f52a";
 }
 }
 .fooicon-remove:before {
 .fooicon-remove:before {
-  content: "\f64f";
+  content: "\f62a";
 }
 }
 .fooicon-sort:before {
 .fooicon-sort:before {
   content: "\f3c6";
   content: "\f3c6";
@@ -147,7 +147,7 @@ table.footable > tbody > tr.footable-empty > th {
   content: "\f4c9";
   content: "\f4c9";
 }
 }
 .fooicon-trash:before {
 .fooicon-trash:before {
-  content: "\f64f";
+  content: "\f62a";
 }
 }
 .fooicon-eye-close:before {
 .fooicon-eye-close:before {
   content: "\f33f";
   content: "\f33f";

+ 8 - 0
data/web/css/site/user.css

@@ -111,4 +111,12 @@ border-bottom-width: 3px;
   padding: .1em .5em .1em;
   padding: .1em .5em .1em;
   font-size: inherit;
   font-size: inherit;
   font-weight: 400;
   font-weight: 400;
+}
+.clear-last-logins {
+  cursor: pointer;
+  margin-top: 10px;
+  font-size:90%;
+  font-style: italic;
+  color: #158cba;
+  user-select:none;
 }
 }

+ 18 - 0
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"><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"><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-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>
       </ul>
     </li>
     </li>
   </ul>
   </ul>
@@ -217,6 +218,23 @@ $xmpp_status = xmpp_control('status');
           </div>
           </div>
         </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 role="tabpanel" class="tab-pane" id="tab-dovecot-logs">
           <div class="panel panel-default">
           <div class="panel panel-default">
             <div class="panel-heading">Dovecot <span class="badge badge-info table-lines"></span>
             <div class="panel-heading">Dovecot <span class="badge badge-info table-lines"></span>

+ 72 - 12
data/web/inc/functions.inc.php

@@ -251,20 +251,60 @@ function password_check($password1, $password2) {
 
 
   return true;
   return true;
 }
 }
-function last_login($user) {
+function last_login($action, $username) {
   global $pdo;
   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() {
 function flush_memcached() {
   try {
   try {
@@ -1862,6 +1902,26 @@ function get_logs($application, $lines = false) {
       return $data;
       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
   // Redis
   if ($application == "dovecot-mailcow") {
   if ($application == "dovecot-mailcow") {
     if (isset($from) && isset($to)) {
     if (isset($from) && isset($to)) {

+ 32 - 16
data/web/inc/functions.mailbox.inc.php

@@ -3503,18 +3503,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             return false;
             return false;
           }
           }
           $mailboxdata = array();
           $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'))) {
           if (preg_match('/y|yes/i', getenv('MASTER'))) {
             $stmt = $pdo->prepare("SELECT
             $stmt = $pdo->prepare("SELECT
               `domain`.`backupmx`,
               `domain`.`backupmx`,
@@ -3575,10 +3563,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $mailboxdata['quota_used'] = intval($row['bytes']);
           $mailboxdata['quota_used'] = intval($row['bytes']);
           $mailboxdata['percent_in_use'] = ($row['quota'] == 0) ? '- ' : round((intval($row['bytes']) / intval($row['quota'])) * 100);
           $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'] === '- ') {
           if ($mailboxdata['percent_in_use'] === '- ') {
             $mailboxdata['percent_class'] = "info";
             $mailboxdata['percent_class'] = "info";
           }
           }
@@ -3592,11 +3576,43 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             $mailboxdata['percent_class'] = "success";
             $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') {
           if (!isset($_extra) || $_extra != 'reduced') {
             $rl = ratelimit('get', 'mailbox', $_data);
             $rl = ratelimit('get', 'mailbox', $_data);
             $stmt = $pdo->prepare("SELECT `maxquota`, `quota` FROM  `domain` WHERE `domain` = :domain");
             $stmt = $pdo->prepare("SELECT `maxquota`, `quota` FROM  `domain` WHERE `domain` = :domain");
             $stmt->execute(array(':domain' => $row['domain']));
             $stmt->execute(array(':domain' => $row['domain']));
             $DomainQuota  = $stmt->fetch(PDO::FETCH_ASSOC);
             $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 = $pdo->prepare("SELECT IFNULL(COUNT(`active`), 0) AS `pushover_active` FROM `pushover` WHERE `username` = :username AND `active` = 1");
             $stmt->execute(array(':username' => $_data));
             $stmt->execute(array(':username' => $_data));
             $PushoverActive  = $stmt->fetch(PDO::FETCH_ASSOC);
             $PushoverActive  = $stmt->fetch(PDO::FETCH_ASSOC);

+ 25 - 1
data/web/inc/init_db.inc.php

@@ -3,7 +3,7 @@ function init_db_schema() {
   try {
   try {
     global $pdo;
     global $pdo;
 
 
-    $db_version = "27052021_2000";
+    $db_version = "03062021_2320";
 
 
     $stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
     $stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
     $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
     $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"
         "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(
       "quota2" => array(
         "cols" => array(
         "cols" => array(
           "username" => "VARCHAR(255) NOT NULL",
           "username" => "VARCHAR(255) NOT NULL",

+ 0 - 3
data/web/inc/triggers.inc.php

@@ -24,19 +24,16 @@ if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
 	if ($as == "admin") {
 	if ($as == "admin") {
 		$_SESSION['mailcow_cc_username'] = $login_user;
 		$_SESSION['mailcow_cc_username'] = $login_user;
 		$_SESSION['mailcow_cc_role'] = "admin";
 		$_SESSION['mailcow_cc_role'] = "admin";
-    $_SESSION['mailcow_cc_last_login'] = last_login($login_user);
 		header("Location: /admin");
 		header("Location: /admin");
 	}
 	}
 	elseif ($as == "domainadmin") {
 	elseif ($as == "domainadmin") {
 		$_SESSION['mailcow_cc_username'] = $login_user;
 		$_SESSION['mailcow_cc_username'] = $login_user;
 		$_SESSION['mailcow_cc_role'] = "domainadmin";
 		$_SESSION['mailcow_cc_role'] = "domainadmin";
-    $_SESSION['mailcow_cc_last_login'] = last_login($login_user);
 		header("Location: /mailbox");
 		header("Location: /mailbox");
 	}
 	}
 	elseif ($as == "user") {
 	elseif ($as == "user") {
 		$_SESSION['mailcow_cc_username'] = $login_user;
 		$_SESSION['mailcow_cc_username'] = $login_user;
 		$_SESSION['mailcow_cc_role'] = "user";
 		$_SESSION['mailcow_cc_role'] = "user";
-    $_SESSION['mailcow_cc_last_login'] = last_login($login_user);
     $http_parameters = explode('&', $_SESSION['index_query_string']);
     $http_parameters = explode('&', $_SESSION['index_query_string']);
     unset($_SESSION['index_query_string']);
     unset($_SESSION['index_query_string']);
     if (in_array('mobileconfig', $http_parameters)) {
     if (in_array('mobileconfig', $http_parameters)) {

+ 53 - 1
data/web/js/site/debug.js

@@ -271,7 +271,7 @@ jQuery(function($){
         {"name":"role","title":"Role"},
         {"name":"role","title":"Role"},
         {"name":"remote","title":"IP"},
         {"name":"remote","title":"IP"},
         {"name":"msg","title":lang.message,"style":{"word-break":"break-all"}},
         {"name":"msg","title":lang.message,"style":{"word-break":"break-all"}},
-        {"name":"call","title":"Call","breakpoints": "all"},
+        {"name":"call","title":"Call","breakpoints": "all"}
       ],
       ],
       "rows": $.ajax({
       "rows": $.ajax({
         dataType: 'json',
         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() {
   function draw_acme_logs() {
     ft_acme_logs = FooTable.init('#acme_log', {
     ft_acme_logs = FooTable.init('#acme_log', {
       "columns": [
       "columns": [
@@ -666,6 +703,20 @@ jQuery(function($){
         item.task = '<code>' + item.task + '</code>';
         item.task = '<code>' + item.task + '</code>';
         item.type = '<span class="label label-' + item.type + '">' + item.type + '</span>';
         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') {
     } else if (table == 'general_syslog') {
       $.each(data, function (i, item) {
       $.each(data, function (i, item) {
         if (item === null) { return true; }
         if (item === null) { return true; }
@@ -750,6 +801,7 @@ jQuery(function($){
   draw_api_logs();
   draw_api_logs();
   draw_rl_logs();
   draw_rl_logs();
   draw_ui_logs();
   draw_ui_logs();
+  draw_sasl_logs();
   draw_netfilter_logs();
   draw_netfilter_logs();
   draw_rspamd_history();
   draw_rspamd_history();
   $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
   $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {

+ 62 - 3
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"); });
   $(".arrow-toggle").on('click', function(e) { e.preventDefault(); $(this).find('.arrow').toggleClass("animation"); });
   $("#pushover_delete").click(function() { return confirm(lang.delete_ays); });
   $("#pushover_delete").click(function() { return confirm(lang.delete_ays); });
+
 });
 });
 jQuery(function($){
 jQuery(function($){
   // http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
   // 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"});
     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);
   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() {
   function draw_tla_table() {
     ft_tla_table = FooTable.init('#tla_table', {
     ft_tla_table = FooTable.init('#tla_table', {
@@ -132,7 +190,7 @@ jQuery(function($){
         {"name":"log","title":"Log"},
         {"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":"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":"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,
       "empty": lang.empty,
       "rows": $.ajax({
       "rows": $.ajax({
@@ -324,6 +382,7 @@ jQuery(function($){
   draw_tla_table();
   draw_tla_table();
   draw_wl_policy_mailbox_table();
   draw_wl_policy_mailbox_table();
   draw_bl_policy_mailbox_table();
   draw_bl_policy_mailbox_table();
+  last_logins('get');
 
 
   // FIDO2 friendly name modal
   // FIDO2 friendly name modal
   $('#fido2ChangeFn').on('show.bs.modal', function (e) {
   $('#fido2ChangeFn').on('show.bs.modal', function (e) {

+ 26 - 2
data/web/json_api.php

@@ -306,7 +306,6 @@ if (isset($_GET['query'])) {
             $_SESSION["mailcow_cc_role"] = "domainadmin";
             $_SESSION["mailcow_cc_role"] = "domainadmin";
           }
           }
           $_SESSION["mailcow_cc_username"] = $process_fido2['username'];
           $_SESSION["mailcow_cc_username"] = $process_fido2['username'];
-          $_SESSION['mailcow_cc_last_login'] = last_login($process_fido2['username']);
           $_SESSION["fido2_cid"] = $process_fido2['cid'];
           $_SESSION["fido2_cid"] = $process_fido2['cid'];
           unset($_SESSION["challenge"]);
           unset($_SESSION["challenge"]);
           $_SESSION['return'][] =  array(
           $_SESSION['return'][] =  array(
@@ -640,6 +639,21 @@ if (isset($_GET['query'])) {
             }
             }
           break;
           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":
           case "transport":
             switch ($object) {
             switch ($object) {
               case "all":
               case "all":
@@ -800,6 +814,17 @@ if (isset($_GET['query'])) {
                 }
                 }
                 echo (isset($logs) && !empty($logs)) ? json_encode($logs, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) : '{}';
                 echo (isset($logs) && !empty($logs)) ? json_encode($logs, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) : '{}';
               break;
               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":
               case "watchdog":
                 // 0 is first record, so empty is fine
                 // 0 is first record, so empty is fine
                 if (isset($extra)) {
                 if (isset($extra)) {
@@ -1458,7 +1483,6 @@ if (isset($_GET['query'])) {
           process_delete_return(dkim('delete', array('domains' => $items)));
           process_delete_return(dkim('delete', array('domains' => $items)));
         break;
         break;
         case "domain":
         case "domain":
-          file_put_contents('/tmp/dssaa', $items);
           process_delete_return(mailbox('delete', 'domain', array('domain' => $items)));
           process_delete_return(mailbox('delete', 'domain', array('domain' => $items)));
         break;
         break;
         case "alias-domain":
         case "alias-domain":

+ 12 - 0
data/web/lang/lang.de.json

@@ -119,6 +119,10 @@
         "validation_success": "Erfolgreich validiert"
         "validation_success": "Erfolgreich validiert"
     },
     },
     "admin": {
     "admin": {
+        "success": "Erfolg",
+        "service": "Dienst",
+        "login_time": "Zeit",
+        "username": "Benutzername",
         "access": "Zugang",
         "access": "Zugang",
         "action": "Aktion",
         "action": "Aktion",
         "activate_api": "API aktivieren",
         "activate_api": "API aktivieren",
@@ -489,6 +493,10 @@
         "started_at": "Gestartet am",
         "started_at": "Gestartet am",
         "solr_status": "Solr Status",
         "solr_status": "Solr Status",
         "uptime": "Uptime",
         "uptime": "Uptime",
+        "success": "Erfolg",
+        "service": "Dienst",
+        "login_time": "Zeit",
+        "username": "Benutzername",
         "started_on": "Gestartet am",
         "started_on": "Gestartet am",
         "static_logs": "Statische Logs",
         "static_logs": "Statische Logs",
         "system_containers": "System & Container",
         "system_containers": "System & Container",
@@ -1032,6 +1040,9 @@
         "excludes": "Ausschlüsse",
         "excludes": "Ausschlüsse",
         "expire_in": "Ungültig in",
         "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.",
         "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",
         "generate": "generieren",
         "hour": "Stunde",
         "hour": "Stunde",
         "hourly": "Stündlich",
         "hourly": "Stündlich",
@@ -1041,6 +1052,7 @@
         "is_catch_all": "Ist Catch-All-Adresse für Domain(s)",
         "is_catch_all": "Ist Catch-All-Adresse für Domain(s)",
         "last_mail_login": "Letzter Mail-Login",
         "last_mail_login": "Letzter Mail-Login",
         "last_run": "Letzte Ausführung",
         "last_run": "Letzte Ausführung",
+        "last_ui_login": "Letzte UI Anmeldung",
         "loading": "Lade...",
         "loading": "Lade...",
         "mailbox_details": "Mailbox-Details",
         "mailbox_details": "Mailbox-Details",
         "messages": "Nachrichten",
         "messages": "Nachrichten",

+ 13 - 1
data/web/lang/lang.en.json

@@ -117,6 +117,10 @@
         "validation_success": "Validated successfully"
         "validation_success": "Validated successfully"
     },
     },
     "admin": {
     "admin": {
+        "success": "Success",
+        "service": "Service",
+        "login_time": "Login time",
+        "username": "Username",
         "access": "Access",
         "access": "Access",
         "action": "Action",
         "action": "Action",
         "activate_api": "Activate API",
         "activate_api": "Activate API",
@@ -486,8 +490,12 @@
         "size": "Size",
         "size": "Size",
         "started_at": "Started at",
         "started_at": "Started at",
         "solr_status": "Solr status",
         "solr_status": "Solr status",
-        "uptime": "Uptime",
         "started_on": "Started on",
         "started_on": "Started on",
+        "uptime": "Uptime",
+        "success": "Success",
+        "service": "Service",
+        "login_time": "Time",
+        "username": "Username",
         "static_logs": "Static logs",
         "static_logs": "Static logs",
         "system_containers": "System & Containers",
         "system_containers": "System & Containers",
         "xmpp_status": "XMPP status"
         "xmpp_status": "XMPP status"
@@ -1030,6 +1038,9 @@
         "excludes": "Excludes",
         "excludes": "Excludes",
         "expire_in": "Expire in",
         "expire_in": "Expire in",
         "force_pw_update": "You <b>must</b> set a new password to be able to access groupware related services.",
         "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",
         "generate": "generate",
         "hour": "hour",
         "hour": "hour",
         "hourly": "Hourly",
         "hourly": "Hourly",
@@ -1039,6 +1050,7 @@
         "is_catch_all": "Catch-all for domain/s",
         "is_catch_all": "Catch-all for domain/s",
         "last_mail_login": "Last mail login",
         "last_mail_login": "Last mail login",
         "last_run": "Last run",
         "last_run": "Last run",
+        "last_ui_login": "Last UI login",
         "loading": "Loading...",
         "loading": "Loading...",
         "mailbox_details": "Mailbox details",
         "mailbox_details": "Mailbox details",
         "messages": "messages",
         "messages": "messages",

+ 6 - 18
data/web/user.php

@@ -22,15 +22,8 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'doma
     <div class="row">
     <div class="row">
       <div class="col-sm-offset-3 col-sm-9">
       <div class="col-sm-offset-3 col-sm-9">
         <p><a href="#pwChangeModal" data-toggle="modal">[<?=$lang['user']['change_password'];?>]</a></p>
         <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>
         <p>
       </div>
       </div>
     </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 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 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><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>
         </div>
         </div>
         <hr>
         <hr>