浏览代码

[BS5] fix merging bugs

FreddleSpl0it 3 年之前
父节点
当前提交
45e97b3753

+ 79 - 1
data/web/css/build/013-mailcow.css

@@ -271,6 +271,28 @@ code {
   margin-right: 5px;
 }
 
+.dropdown-header {
+  font-weight: 600;
+}
+
+.dataTables_info {
+  margin: 15px 0 !important;
+  padding: 0px !important;
+}
+.dataTables_paginate, .dataTables_length, .dataTables_filter {
+  margin: 15px 0 !important;
+}
+.dtr-details {
+  width: 100%;
+}
+.dtr-title {
+  width: 20%;
+}
+table.dataTable>tbody>tr.child ul.dtr-details>li {
+  border-bottom: 1px solid rgba(239, 239, 239, 0.129);
+  padding: 0.5em 0;
+}
+
 .tag-box {
   display: flex;
   flex-wrap: wrap;
@@ -315,4 +337,60 @@ code {
   border: 1px solid #dfdfdf;
   background-color: #f9f9f9;
   padding: 10px;
-}
+}
+
+table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before:hover, 
+table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before:hover {
+  background-color: #5e5e5e;
+}
+table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before, 
+table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before,
+table.dataTable td.dt-control:before {
+  background-color: #979797 !important;
+  border: 1.5px solid #616161 !important;
+  border-radius: 2px !important;
+  color: #fff;
+  height: 1em;
+  width: 1em;
+  line-height: 1.25em;
+  border-radius: 0px;
+  box-shadow: none;
+  font-size: 14px;
+  transition: 0.5s all;
+}
+table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td.dtr-control:before, 
+table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th.dtr-control:before,
+table.dataTable td.dt-control:before {
+  background-color: #979797 !important;
+}
+table.dataTable.dtr-inline.collapsed>tbody>tr>td.child, 
+table.dataTable.dtr-inline.collapsed>tbody>tr>th.child, 
+table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty {
+  background-color: #fbfbfb;
+}
+table.dataTable.table-striped>tbody>tr>td {
+  vertical-align: middle;
+}
+table.dataTable.table-striped>tbody>tr>td>input[type="checkbox"] {
+  margin-top: 7px;
+}
+
+
+.btn-check-label {
+  color: #555;
+}
+
+.caret {
+  transform: rotate(0deg);
+}
+a[aria-expanded='true'] > .caret, 
+button[aria-expanded='true'] > .caret {
+  transform: rotate(-180deg);
+}
+
+.list-group-details {
+  background: #fff;
+}
+.list-group-header {
+  background: #f7f7f7;
+} 

+ 1 - 1
data/web/css/themes/mailcow-darkmode.css

@@ -48,7 +48,7 @@ legend {
     border-color: #7a7a7a !important;
 }
 .modal-content {
-    background-color: #383838;
+    background-color: #414141;
 }
 .modal-header {
     border-bottom: 1px solid #161616;

+ 0 - 2
data/web/js/site/admin.js

@@ -77,7 +77,6 @@ jQuery(function($){
         type: "GET",
         url: "/api/v1/get/domain-admin/all",
         dataSrc: function(data){
-          console.log(data);
           return process_table_data(data, 'domainadminstable');
         }
       },
@@ -133,7 +132,6 @@ jQuery(function($){
           },
       ],
       initComplete: function(settings, json){
-        console.log(settings);
       }
     });
   }

+ 75 - 65
data/web/js/site/debug.js

@@ -44,7 +44,9 @@ $(document).ready(function() {
   // create host cpu and mem charts
   createHostCpuAndMemChart();
   // check for new version
-  check_update(mailcow_info.version_tag, mailcow_info.project_url);
+  if (mailcow_info.branch === "master"){
+    check_update(mailcow_info.version_tag, mailcow_info.project_url);
+  }
   update_container_stats()
 });
 jQuery(function($){
@@ -1108,75 +1110,83 @@ function update_stats(timeout=5){
 }
 // update specific container stats - every n (default 5s) seconds
 function update_container_stats(timeout=5){
-  for (let container in containersToUpdate){
-    container_id = containersToUpdate[container].id;
-    if (containersToUpdate[container].state == "running")
-      continue;
-    containersToUpdate[container].state = "running";
+  
+  if ($('#tab-containers').hasClass('active')) {
+    for (let container in containersToUpdate){
+      container_id = containersToUpdate[container].id;
+      // check if container update stats is already running
+      if (containersToUpdate[container].state == "running")
+        continue;
+      containersToUpdate[container].state = "running";
 
 
-    window.fetch("/api/v1/get/status/container/" + container_id, {method:'GET',cache:'no-cache'}).then(function(response) {
-      return response.json();
-    }).then(function(data) {
-      var diskIOCtx = Chart.getChart(container + "_DiskIOChart");
-      var netIOCtx = Chart.getChart(container + "_NetIOChart");
+      window.fetch("/api/v1/get/status/container/" + container_id, {method:'GET',cache:'no-cache'}).then(function(response) {
+        return response.json();
+      }).then(function(data) {
+        var diskIOCtx = Chart.getChart(container + "_DiskIOChart");
+        var netIOCtx = Chart.getChart(container + "_NetIOChart");
 
-      console.log(container);
-      console.log(data);
-      prev_stats = null;
-      if (data.length >= 2)
-        prev_stats = data[data.length -2]
-      data = data[data.length -1];
+        console.log(container);
+        console.log(data);
+        prev_stats = null;
+        if (data.length >= 2)
+          prev_stats = data[data.length -2]
+        data = data[data.length -1];
 
-      if (prev_stats != null){
-        // calc time diff
-        var time_diff = (new Date(data.read) - new Date(prev_stats.read)) / 1000;
-  
-        // calc disk io b/s
-        var prev_read_bytes = 0;
-        var prev_write_bytes = 0;
-        for (var i = 0; i < prev_stats.blkio_stats.io_service_bytes_recursive.length; i++){
-          if (prev_stats.blkio_stats.io_service_bytes_recursive[i].op == "read")
-            prev_read_bytes = prev_stats.blkio_stats.io_service_bytes_recursive[i].value;
-          else if (prev_stats.blkio_stats.io_service_bytes_recursive[i].op == "write")
-            prev_write_bytes = prev_stats.blkio_stats.io_service_bytes_recursive[i].value;
-        }
-        var read_bytes = 0;
-        var write_bytes = 0;
-        for (var i = 0; i < data.blkio_stats.io_service_bytes_recursive.length; i++){
-          if (data.blkio_stats.io_service_bytes_recursive[i].op == "read")
-            read_bytes = data.blkio_stats.io_service_bytes_recursive[i].value;
-          else if (data.blkio_stats.io_service_bytes_recursive[i].op == "write")
-            write_bytes = data.blkio_stats.io_service_bytes_recursive[i].value;
-        }
-        var diff_bytes_read = (read_bytes - prev_read_bytes) / time_diff;
-        var diff_bytes_write = (write_bytes - prev_write_bytes) / time_diff;
-  
-        // calc net io b/s
-        var prev_recv_bytes = 0;
-        var prev_sent_bytes = 0;
-        for (var key in prev_stats.networks){
-          prev_recv_bytes += prev_stats.networks[key].rx_bytes;
-          prev_sent_bytes += prev_stats.networks[key].tx_bytes;
-        }
-        var recv_bytes = 0;
-        var sent_bytes = 0;
-        for (var key in data.networks){
-          recv_bytes += data.networks[key].rx_bytes;
-          sent_bytes += data.networks[key].tx_bytes;
+        if (prev_stats != null){
+          // calc time diff
+          var time_diff = (new Date(data.read) - new Date(prev_stats.read)) / 1000;
+    
+          // calc disk io b/s
+          if ('io_service_bytes_recursive' in prev_stats.blkio_stats && prev_stats.blkio_stats.io_service_bytes_recursive !== null){
+            var prev_read_bytes = 0;
+            var prev_write_bytes = 0;
+            for (var i = 0; i < prev_stats.blkio_stats.io_service_bytes_recursive.length; i++){
+              if (prev_stats.blkio_stats.io_service_bytes_recursive[i].op == "read")
+                prev_read_bytes = prev_stats.blkio_stats.io_service_bytes_recursive[i].value;
+              else if (prev_stats.blkio_stats.io_service_bytes_recursive[i].op == "write")
+                prev_write_bytes = prev_stats.blkio_stats.io_service_bytes_recursive[i].value;
+            }
+            var read_bytes = 0;
+            var write_bytes = 0;
+            for (var i = 0; i < data.blkio_stats.io_service_bytes_recursive.length; i++){
+              if (data.blkio_stats.io_service_bytes_recursive[i].op == "read")
+                read_bytes = data.blkio_stats.io_service_bytes_recursive[i].value;
+              else if (data.blkio_stats.io_service_bytes_recursive[i].op == "write")
+                write_bytes = data.blkio_stats.io_service_bytes_recursive[i].value;
+            }
+            var diff_bytes_read = (read_bytes - prev_read_bytes) / time_diff;
+            var diff_bytes_write = (write_bytes - prev_write_bytes) / time_diff;
+          }
+    
+          // calc net io b/s
+          if ('networks' in prev_stats){
+            var prev_recv_bytes = 0;
+            var prev_sent_bytes = 0;
+            for (var key in prev_stats.networks){
+              prev_recv_bytes += prev_stats.networks[key].rx_bytes;
+              prev_sent_bytes += prev_stats.networks[key].tx_bytes;
+            }
+            var recv_bytes = 0;
+            var sent_bytes = 0;
+            for (var key in data.networks){
+              recv_bytes += data.networks[key].rx_bytes;
+              sent_bytes += data.networks[key].tx_bytes;
+            }
+            var diff_bytes_recv = (recv_bytes - prev_recv_bytes) / time_diff;
+            var diff_bytes_sent = (sent_bytes - prev_sent_bytes) / time_diff;
+          }
+    
+          addReadWriteChart(diskIOCtx, diff_bytes_read, diff_bytes_write, "");
+          addReadWriteChart(netIOCtx, diff_bytes_recv, diff_bytes_sent, "");
         }
-        var diff_bytes_recv = (recv_bytes - prev_recv_bytes) / time_diff;
-        var diff_bytes_sent = (sent_bytes - prev_sent_bytes) / time_diff;
-  
-        addReadWriteChart(diskIOCtx, diff_bytes_read, diff_bytes_write, "");
-        addReadWriteChart(netIOCtx, diff_bytes_recv, diff_bytes_sent, "");
-      }
-  
-      // run again in n seconds
-      containersToUpdate[container].state = "idle";
-    }).catch(err => {
-      console.log(err);
-    });
+    
+        // run again in n seconds
+        containersToUpdate[container].state = "idle";
+      }).catch(err => {
+        console.log(err);
+      });
+    }
   }
 
   // run again in n seconds

+ 0 - 1
data/web/templates/admin/tab-config-admins.twig

@@ -32,7 +32,6 @@
       <div class="row">
         <div class="col-sm-3 col-5 text-end">{{ lang.tfa.tfa }}:</div>
         <div class="col-sm-9 col-7">
-          <p id="tfa_pretty">{{ tfa_data.pretty }}</p>
           {% include 'tfa_keys.twig' %}
           <br>
         </div>

+ 12 - 3
data/web/templates/base.twig

@@ -143,7 +143,8 @@
     updatedAt: '{{ mailcow_info.updatedAt }}',
     project_url: '{{ mailcow_info.project_url }}',
     project_owner: '{{ mailcow_info.project_owner }}',
-    project_repo: '{{ mailcow_info.project_repo }}'
+    project_repo: '{{ mailcow_info.project_repo }}',
+    branch: '{{ mailcow_info.mailcow_branch }}'
   };
 
 $(window).scroll(function() {
@@ -202,7 +203,7 @@ function recursiveBase64StrToArrayBuffer(obj) {
     {% endfor %}
 
     // Confirm TFA modal
-  {% if pending_tfa_method %}
+  {% if pending_tfa_methods %}
     new bootstrap.Modal(document.getElementById("ConfirmTFAModal"), {
       backdrop: 'static',
       keyboard: false
@@ -270,6 +271,11 @@ function recursiveBase64StrToArrayBuffer(obj) {
       var id = $(this).children('input').first().val();
       $("#webauthn_selected_id").val(id);
       
+      var webauthn_status_auth = document.getElementById('webauthn_status_auth');
+      webauthn_status_auth.style.setProperty('display', 'flex', 'important');
+      var webauthn_return_code = document.getElementById('webauthn_return_code');
+      webauthn_return_code.style.setProperty('display', 'none', 'important');
+
       $("#collapseWebAuthnTFA").collapse('show');
 
       $(this).find('input[name=token]').focus();
@@ -307,8 +313,11 @@ function recursiveBase64StrToArrayBuffer(obj) {
           auth.value = AuthenticatorAttestationResponse;
           form.submit();
         }).catch(function(err) {
+          var webauthn_status_auth = document.getElementById('webauthn_status_auth');
+          webauthn_status_auth.style.setProperty('display', 'none', 'important');
+
           var webauthn_return_code = document.getElementById('webauthn_return_code');
-          webauthn_return_code.style.display = webauthn_return_code.style.display === 'none' ? '' : null;
+          webauthn_return_code.style.setProperty('display', 'block', 'important');
           webauthn_return_code.innerHTML = lang_tfa.error_code + ': ' + err + ' ' + lang_tfa.reload_retry;
         });
       } 

+ 2 - 0
data/web/templates/debug.twig

@@ -56,12 +56,14 @@
                           <p id="mailcow_update"></p> 
                         </div></td>
                       </tr>
+                      {% if mailcow_info.mailcow_branch|lower == "master" %}
                       <tr>
                         <td>Changelog</td>
                         <td class="text-break"><a href="{{ mailcow_info.project_url }}/releases/tag/{{ mailcow_info.version_tag }}" target="_blank">
                           {{ mailcow_info.project_url }}/releases/tag/{{ mailcow_info.version_tag }}
                         </a></td>
                       </tr>
+                      {% endif %}
                       <tr>
                         <td>{{ lang.debug.current_time }}</td>
                         <td id="host_date" class="text-break">-</td>

+ 1 - 1
data/web/templates/edit/mailbox.twig

@@ -40,7 +40,7 @@
       </div>
       <div class="row mb-2">
         <label class="control-label col-sm-2" for="quota">{{ lang.edit.quota_mb }}
-          <br><span id="quotaBadge" class="badge">max. {{ (result.max_new_quota / 1048576) }} MiB</span>
+          <br><span id="quotaBadge" class="badge bg-info">max. {{ (result.max_new_quota / 1048576) }} MiB</span>
         </label>
         <div class="col-sm-10">
           <input type="number" name="quota" style="width:100%" min="0" max="{{ (result.max_new_quota / 1048576) }}" value="{{ (result.quota / 1048576) }}" class="form-control">

+ 166 - 57
data/web/templates/modals/footer.twig

@@ -136,67 +136,176 @@
   <div class="modal-dialog" role="document">
     <div class="modal-content">
       <div class="modal-header">
-        <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span></button>
-        <h3 class="modal-title">{{ lang.tfa[pending_tfa_method] }}</h3>
+        <h3 class="modal-title">{{ lang.tfa.tfa }}</h3>
+        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
       </div>
-      <div class="modal-body">
-        {% if pending_tfa_method == 'yubi_otp' %}
-        <form role="form" method="post">
-          <div class="form-group">
-            <div class="input-group">
-              <span class="input-group-addon" id="yubi-addon"><img alt="Yubicon Icon" src="/img/yubi.ico"></span>
-              <input type="text" name="token" class="form-control" autocomplete="off" placeholder="Touch Yubikey" aria-describedby="yubi-addon">
-              <input type="hidden" name="tfa_method" value="yubi_otp">
+      
+      
+      <div class="modal-body p-0 pt-4">
+        <ul class="nav nav-tabs px-1" id="tabContent">
+            {% if pending_tfa_authmechs["webauthn"] is defined and pending_tfa_authmechs["u2f"] is not defined %}
+              <li class="nav-item"><a class="nav-link active" href="#tfa_tab_webauthn" data-bs-toggle="tab" id="pending_tfa_tab_webauthn"><i class="bi bi-fingerprint"></i> WebAuthn</a></li>
+            {% endif %}
+
+            {% if pending_tfa_authmechs["yubi_otp"] is defined and pending_tfa_authmechs["u2f"] is not defined %}
+              <li class="nav-item">
+                <a class="nav-link {% if pending_tfa_authmechs["yubi_otp"] %}active{% endif %}" href="#tfa_tab_yubi_otp" data-bs-toggle="tab" id="pending_tfa_tab_yubi_otp"><i class="bi bi-usb-drive"></i> Yubi OTP</a>
+              </li>
+            {% endif %}
+
+            {% if pending_tfa_authmechs["totp"] is defined and pending_tfa_authmechs["u2f"] is not defined %}
+              <li class="nav-item">
+                <a class="nav-link {% if pending_tfa_authmechs["totp"] %}active{% endif %}" href="#tfa_tab_totp" data-bs-toggle="tab" id="pending_tfa_tab_totp"><i class="bi bi-clock-history"></i> Time based OTP</a>
+              </li>
+            {% endif %}
+
+            <!-- <li class="nav-item"><a class="nav-link" href="#tfa_tab_hotp" data-bs-toggle="tab">HOTP</a></li> -->
+            {% if pending_tfa_authmechs["u2f"] is defined %}
+              <li class="nav-item"><a class="nav-link active" href="#tfa_tab_u2f" data-bs-toggle="tab"><i class="bi bi-x-octagon"></i> U2F</a></li>
+            {% endif %}
+        </ul>
+
+        <div class="tab-content">
+          {% if pending_tfa_authmechs["webauthn"] is defined and pending_tfa_authmechs["u2f"] is not defined %}
+            <div role="tabpanel" class="tab-pane active" id="tfa_tab_webauthn">
+              <div class="card border-0" style="margin-bottom: 0px;">
+                  <div class="card-body">
+                    <form role="form" method="post" id="webauthn_auth_form">
+                      <legend class="mt-2 mb-2">
+                          <i class="bi bi-shield-fill-check"></i>
+                          Authenticators
+                          <hr />
+                      </legend>
+                      <div class="list-group">
+                        {% for authenticator in pending_tfa_methods %}
+                          {% if authenticator["authmech"] == "webauthn" %}
+                            <a href="#" class="list-group-item webauthn-authenticator-selection">
+                              <i class="bi bi-key-fill" style="margin-right: 5px"></i>
+                              <span>{{ authenticator["key_id"] }}</span>
+                              <input type="hidden" value="{{ authenticator["id"] }}" /><br/>
+                            </a>
+                          {% endif %}
+                        {% endfor %}
+                      </div>
+                      <div class="collapse pending-tfa-collapse" id="collapseWebAuthnTFA">
+                        <div class="row mt-4 mb-4">
+                          <div class="col-12">
+                            <div class="mt-4 d-flex align-items-center" id="webauthn_status_auth">
+                              <div class="spinner-border me-2" role="status">
+                                <span class="visually-hidden">Loading...</span>
+                              </div>
+                              {{ lang.tfa.init_webauthn }}
+                            </div>
+                            <div class="mt-4 alert alert-danger" style="display:none" id="webauthn_return_code"></div>
+                          </div>
+                        </div>
+                      </div>
+                      <input type="hidden" name="token" id="webauthn_auth_data"/>
+                      <input type="hidden" name="tfa_method" value="webauthn">
+                      <input type="hidden" name="verify_tfa_login"/>
+                      <input type="hidden" name="id" id="webauthn_selected_id" /><br/>
+                    </form>
+                  </div>
+              </div>
             </div>
-          </div>
-          <button class="btn btn-sm visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-sm btn-default" type="submit" name="verify_tfa_login">{{ lang.login.login }}</button>
-        </form>
-        {% endif %}
-        {% if pending_tfa_method == 'totp' %}
-        <form role="form" method="post">
-          <div class="form-group">
-            <div class="input-group">
-              <span class="input-group-addon" id="tfa-addon"><i class="bi bi-shield-lock-fill"></i></span>
-              <input type="number" min="000000" max="999999" name="token" class="form-control" placeholder="123456" autocomplete="one-time-code" aria-describedby="tfa-addon">
-              <input type="hidden" name="tfa_method" value="totp">
+          {% endif %}
+          {% if pending_tfa_authmechs["yubi_otp"] is defined and pending_tfa_authmechs["u2f"] is not defined %}
+            <div role="tabpanel" class="tab-pane {% if pending_tfa_authmechs["yubi_otp"] %}active{% endif %}" id="tfa_tab_yubi_otp">
+              <div class="card border-0" style="margin-bottom: 0px;">
+                  <div class="card-body">
+                    <form role="form" method="post">
+                      <legend class="mt-2 mb-2">
+                          <i class="bi bi-shield-fill-check"></i>
+                          Authenticate
+                          <hr />
+                      </legend>
+                      <div class="collapse show pending-tfa-collapse" id="collapseYubiTFA">
+                        <div class="row mt-4 mb-4">
+                          <div class="col-12">
+                            <div class="input-group mt-4">
+                              <span class="input-group-text" id="yubi-addon"><img alt="Yubicon Icon" src="/img/yubi.ico"></span>
+                              <input type="text" name="token" class="form-control" autocomplete="off" placeholder="Touch Yubikey" aria-describedby="yubi-addon">
+                              <input type="hidden" name="tfa_method" value="yubi_otp">
+                              <input type="hidden" name="id" id="yubi_selected_id" />
+                            </div>
+                          </div>
+                        </div>
+                        <button class="btn btn-sm visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-secondary" type="submit" name="verify_tfa_login">{{ lang.login.login }}</button>
+                      </div>
+                    </form>
+                  </div>
+              </div>
             </div>
-          </div>
-          <button class="btn btn-sm visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-default" type="submit" name="verify_tfa_login">{{ lang.login.login }}</button>
-        </form>
-        {% endif %}
-        {% if pending_tfa_method == 'hotp' %}
-        <div class="empty"></div>
-        {% endif %}
-
-        {% if pending_tfa_method == 'webauthn' %}
-        <form role="form" method="post" id="webauthn_auth_form">
-          <center>
-            <div style="cursor:pointer" id="start_webauthn_confirmation">
-              <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24">
-                <path d="M17.81 4.47c-.08 0-.16-.02-.23-.06C15.66 3.42 14 3 12.01 3c-1.98 0-3.86.47-5.57 1.41-.24.13-.54.04-.68-.2-.13-.24-.04-.55.2-.68C7.82 2.52 9.86 2 12.01 2c2.13 0 3.99.47 6.03 1.52.25.13.34.43.21.67-.09.18-.26.28-.44.28zM3.5 9.72c-.1 0-.2-.03-.29-.09-.23-.16-.28-.47-.12-.7.99-1.4 2.25-2.5 3.75-3.27C9.98 4.04 14 4.03 17.15 5.65c1.5.77 2.76 1.86 3.75 3.25.16.22.11.54-.12.7-.23.16-.54.11-.7-.12-.9-1.26-2.04-2.25-3.39-2.94-2.87-1.47-6.54-1.47-9.4.01-1.36.7-2.5 1.7-3.4 2.96-.08.14-.23.21-.39.21zm6.25 12.07c-.13 0-.26-.05-.35-.15-.87-.87-1.34-1.43-2.01-2.64-.69-1.23-1.05-2.73-1.05-4.34 0-2.97 2.54-5.39 5.66-5.39s5.66 2.42 5.66 5.39c0 .28-.22.5-.5.5s-.5-.22-.5-.5c0-2.42-2.09-4.39-4.66-4.39-2.57 0-4.66 1.97-4.66 4.39 0 1.44.32 2.77.93 3.85.64 1.15 1.08 1.64 1.85 2.42.19.2.19.51 0 .71-.11.1-.24.15-.37.15zm7.17-1.85c-1.19 0-2.24-.3-3.1-.89-1.49-1.01-2.38-2.65-2.38-4.39 0-.28.22-.5.5-.5s.5.22.5.5c0 1.41.72 2.74 1.94 3.56.71.48 1.54.71 2.54.71.24 0 .64-.03 1.04-.1.27-.05.53.13.58.41.05.27-.13.53-.41.58-.57.11-1.07.12-1.21.12zM14.91 22c-.04 0-.09-.01-.13-.02-1.59-.44-2.63-1.03-3.72-2.1-1.4-1.39-2.17-3.24-2.17-5.22 0-1.62 1.38-2.94 3.08-2.94 1.7 0 3.08 1.32 3.08 2.94 0 1.07.93 1.94 2.08 1.94s2.08-.87 2.08-1.94c0-3.77-3.25-6.83-7.25-6.83-2.84 0-5.44 1.58-6.61 4.03-.39.81-.59 1.76-.59 2.8 0 .78.07 2.01.67 3.61.1.26-.03.55-.29.64-.26.1-.55-.04-.64-.29-.49-1.31-.73-2.61-.73-3.96 0-1.2.23-2.29.68-3.24 1.33-2.79 4.28-4.6 7.51-4.6 4.55 0 8.25 3.51 8.25 7.83 0 1.62-1.38 2.94-3.08 2.94s-3.08-1.32-3.08-2.94c0-1.07-.93-1.94-2.08-1.94s-2.08.87-2.08 1.94c0 1.71.66 3.31 1.87 4.51.95.94 1.86 1.46 3.27 1.85.27.07.42.35.35.61-.05.23-.26.38-.47.38z"></path>
-              </svg>
-              <p>{{ lang.tfa.start_webauthn_validation }}</p>
-              <hr>
+          {% endif %}
+          {% if pending_tfa_authmechs["totp"] is defined and pending_tfa_authmechs["u2f"] is not defined %}
+            <div role="tabpanel" class="tab-pane {% if pending_tfa_authmechs["totp"] %}active{% endif %}" id="tfa_tab_totp">
+              <div class="card border-0" style="margin-bottom: 0px;">
+                  <div class="card-body">
+                    <form role="form" method="post">        
+                      <legend class="mt-2 mb-2">
+                          <i class="bi bi-shield-fill-check"></i>
+                          Authenticators
+                          <hr />
+                      </legend>
+                      <div class="list-group">
+                        {% for authenticator in pending_tfa_methods %}
+                          {% if authenticator["authmech"] == "totp" %}
+                            <a href="#" class="list-group-item totp-authenticator-selection">
+                              <i class="bi bi-key-fill" style="margin-right: 5px"></i>
+                              <span>{{ authenticator["key_id"] }}</span>
+                              <input type="hidden" value="{{ authenticator["id"] }}" />
+                            </a>
+                          {% endif %}
+                        {% endfor %}
+                      </div>
+                      <div class="collapse pending-tfa-collapse" id="collapseTotpTFA">
+                        <div class="row mt-4 mb-4">
+                          <div class="col-12">
+                            <div class="input-group mt-4">
+                              <span class="input-group-text" id="tfa-addon"><i class="bi bi-shield-lock-fill"></i></span>
+                              <input type="number" min="000000" max="999999" name="token" class="form-control" placeholder="123456" autocomplete="one-time-code" aria-describedby="tfa-addon">
+                              <input type="hidden" name="tfa_method" value="totp">
+                              <input type="hidden" name="id" id="totp_selected_id" /><br/>
+                            </div>
+                            <button class="mt-4 btn btn-sm visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-secondary" type="submit" name="verify_tfa_login">{{ lang.login.login }}</button>
+                          </div>
+                        </div>
+                      </div>
+                    </form>
+                  </div>
+              </div>
             </div>
-          </center>
-          <p id="webauthn_status_auth"></p>
-          <div class="alert alert-danger" style="display:none" id="webauthn_return_code"></div>
-          <input type="hidden" name="token" id="webauthn_auth_data"/>
-          <input type="hidden" name="tfa_method" value="webauthn">
-          <input type="hidden" name="verify_tfa_login"/><br/>
-        </form>
-        {% endif %}
-        {# leave this here to inform users that u2f is deprecated #}
-        {% if pending_tfa_method == 'u2f' %}
-        <form role="form" method="post" id="u2f_auth_form">
-          <p>{{ lang.tfa.u2f_deprecated }}</p>
-          <p><b>{{ lang.tfa.u2f_deprecated_important }}</b></p>
-          <input type="hidden" name="token" value="destroy" />
-          <input type="hidden" name="tfa_method" value="u2f">
-          <input type="hidden" name="verify_tfa_login"/><br/>
-          <button type="submit" class="btn btn-xs-lg btn-success" value="Login">{{ lang.login.login }}</button>
-        </form>
-        {% endif %}
+          {% endif %}
+            <!--
+            <div role="tabpanel" class="tab-pane" id="tfa_tab_hotp">
+              <div class="card" style="margin-bottom: 0px;">
+                  <div class="card-body">
+                      <div class="empty"></div>
+                  </div>
+              </div>
+            </div>
+            -->
+          {% if pending_tfa_authmechs["u2f"] is defined %}
+            <div role="tabpanel" class="tab-pane active" id="tfa_tab_u2f">
+              <div class="card border-0" style="margin-bottom: 0px;">
+                  <div class="card-body">
+                    {# leave this here to inform users that u2f is deprecated #}
+                    <form role="form" method="post" id="u2f_auth_form">
+                      <div>
+                        <p>{{ lang.tfa.u2f_deprecated }}</p>
+                        <p><b>{{ lang.tfa.u2f_deprecated_important }}</b></p>
+                        <input type="hidden" name="token" value="destroy" />
+                        <input type="hidden" name="tfa_method" value="u2f">
+                        <input type="hidden" name="verify_tfa_login"/><br/>
+                        <button type="submit" class="btn btn-xs-lg btn-success" value="Login">{{ lang.login.login }}</button>
+                      </div>
+                    </form>
+                  </div>
+              </div>
+            </div>
+          {% endif %}
+        </div>
+
       </div>
     </div>
   </div>

+ 3 - 3
data/web/templates/tfa_keys.twig

@@ -3,12 +3,12 @@
     {% for key_info in tfa_data.additional %}
       <form style="display:inline;" method="post">
         <input type="hidden" name="unset_tfa_key" value="{{ key_info.id }}">
-        <p>
-          <span style="padding:4px;margin:4px" class="badge fs-6 bg-{% if tfa_id == key_info.id %}success{% else %}dark{% endif %}">
+        <div class="d-flex flex-column">
+          <span style="padding:4px;margin:4px" class="me-auto badge fs-6 bg-{% if tfa_id == key_info.id %}success{% else %}dark{% endif %}">
             {{ key_info.key_id }}
             <a href="#" class="btn p-0 text-white" style="font-size: 12px; line-height: 1rem;" onClick='return confirm("{{ lang.admin.ays }}")?$(this).closest("form").submit():"";'>[{{ lang.admin.remove }}]</a>
           </span>
-        </p>
+        </div>
       </form>
     {% endfor %}
   {% endif %}

+ 1 - 1
data/web/templates/user/tab-user-auth.twig

@@ -65,7 +65,7 @@
       <div class="row">
         <div class="col-sm-3 col-xs-5 text-right">{{ lang.tfa.set_tfa }}:</div>
         <div class="col-sm-9 col-xs-7">
-          <select data-style="btn btn-sm dropdown-toggle bs-placeholder btn-default" data-width="fit" id="selectTFA" class="selectpicker" title="{{ lang.tfa.select }}">
+          <select data-style="btn btn-sm dropdown-toggle bs-placeholder btn-secondary" data-width="fit" id="selectTFA" class="selectpicker" title="{{ lang.tfa.select }}">
             <option value="yubi_otp">{{ lang.tfa.yubi_otp }}</option>
             <option value="webauthn">{{ lang.tfa.webauthn }}</option>
             <option value="totp">{{ lang.tfa.totp }}</option>