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
[BS5] fix merging bugs
45e97b37
data/web/css/build/013-mailcow.css | 80 ++++++++-
data/web/css/themes/mailcow-darkmode.css | 2 +-
data/web/js/site/admin.js | 2 -
data/web/js/site/debug.js | 140 ++++++++-------
data/web/templates/admin/tab-config-admins.twig | 1 -
data/web/templates/base.twig | 15 +-
data/web/templates/debug.twig | 2 +
data/web/templates/edit/mailbox.twig | 2 +-
data/web/templates/modals/footer.twig | 223 ++++++++++++++++++------
data/web/templates/tfa_keys.twig | 6 +-
data/web/templates/user/tab-user-auth.twig | 2 +-
11 files changed, 340 insertions(+), 135 deletions(-)
Diff
diff --git a/data/web/css/build/013-mailcow.css b/data/web/css/build/013-mailcow.css
index 88d82e0c..685eb80d 100644
--- a/data/web/css/build/013-mailcow.css
+++ b/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;
-}
\ No newline at end of file
+}
+
+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;
+}
diff --git a/data/web/css/themes/mailcow-darkmode.css b/data/web/css/themes/mailcow-darkmode.css
index a0388aa0..7a84ddaa 100644
--- a/data/web/css/themes/mailcow-darkmode.css
+++ b/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;
diff --git a/data/web/js/site/admin.js b/data/web/js/site/admin.js
index 61268b02..dcedf856 100644
--- a/data/web/js/site/admin.js
+++ b/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);
}
});
}
diff --git a/data/web/js/site/debug.js b/data/web/js/site/debug.js
index 0575c5a0..1e633673 100644
--- a/data/web/js/site/debug.js
+++ b/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
diff --git a/data/web/templates/admin/tab-config-admins.twig b/data/web/templates/admin/tab-config-admins.twig
index 1d903b56..1d2d05f2 100644
--- a/data/web/templates/admin/tab-config-admins.twig
+++ b/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>
diff --git a/data/web/templates/base.twig b/data/web/templates/base.twig
index 3a34f6ba..70de0234 100644
--- a/data/web/templates/base.twig
+++ b/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;
});
}
diff --git a/data/web/templates/debug.twig b/data/web/templates/debug.twig
index 61847759..3465da6e 100644
--- a/data/web/templates/debug.twig
+++ b/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>
diff --git a/data/web/templates/edit/mailbox.twig b/data/web/templates/edit/mailbox.twig
index a503a151..0f72afc6 100644
--- a/data/web/templates/edit/mailbox.twig
+++ b/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">
diff --git a/data/web/templates/modals/footer.twig b/data/web/templates/modals/footer.twig
index 87e36514..d8b2b5a4 100644
--- a/data/web/templates/modals/footer.twig
+++ b/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>
diff --git a/data/web/templates/tfa_keys.twig b/data/web/templates/tfa_keys.twig
index e73c8916..c74571b9 100644
--- a/data/web/templates/tfa_keys.twig
+++ b/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 %}
diff --git a/data/web/templates/user/tab-user-auth.twig b/data/web/templates/user/tab-user-auth.twig
index 5f432872..d34d2613 100644
--- a/data/web/templates/user/tab-user-auth.twig
+++ b/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>