base.twig 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. <!DOCTYPE html>
  2. <html lang="{{ mailcow_locale|default('en') }}">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
  7. <meta name="theme-color" content="#F5D76E"/>
  8. <meta http-equiv="Referrer-Policy" content="same-origin">
  9. <title>{{ ui_texts.title_name|raw }}</title>
  10. <link rel="stylesheet" href="{{ css_path }}">
  11. <script>
  12. // check if darkmode is preferred by OS or set by localStorage
  13. if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches && localStorage.getItem("theme") !== "light" ||
  14. localStorage.getItem("theme") === "dark") {
  15. var head = document.getElementsByTagName('head')[0];
  16. var link = document.createElement('link');
  17. link.id = 'dark-mode-theme';
  18. link.rel = 'stylesheet';
  19. link.type = 'text/css';
  20. link.href = '/css/themes/mailcow-darkmode.css';
  21. head.appendChild(link);
  22. }
  23. </script>
  24. <link rel="shortcut icon" href="/favicon.png" type="image/png">
  25. <link rel="icon" href="/favicon.png" type="image/png">
  26. </head>
  27. <body>
  28. <div class="overlay"></div>
  29. {% block navbar %}
  30. <nav class="navbar navbar-expand-lg navbar-light bg-light navbar-fixed-top p-0">
  31. <div class="container-fluid">
  32. <a class="navbar-brand" href="/"><img alt="mailcow-logo" src="{{ logo|default('/img/cow_mailcow.svg') }}"></a>
  33. <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
  34. <i class="bi bi-list fs-3"></i>
  35. </button>
  36. <div id="navbar" class="navbar-collapse collapse">
  37. <ul class="navbar-nav ms-auto">
  38. <li class="nav-item">
  39. <div class="nav-link form-check form-switch my-auto d-flex align-items-center">
  40. <label class="form-check-label"><i class="bi bi-moon-fill"></i></label>
  41. <input class="form-check-input ms-2" type="checkbox" id="dark-mode-toggle">
  42. </div>
  43. </li>
  44. {% if mailcow_locale %}
  45. <li class="nav-item dropdown{% if available_languages|length == 1 %}lang-link-disabled{% endif %}">
  46. <a href="#" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" role="button" aria-expanded="false"><span class="flag-icon flag-icon-{{ mailcow_locale[-2:] }}"></span></a>
  47. <ul class="dropdown-menu" role="menu "aria-labelledby="languageDropdown">
  48. {% for key, val in available_languages %}
  49. <li>
  50. <a class="dropdown-item {% if mailcow_locale == key %}active{% endif %}" href="?{{ query_string({'lang': key}) }}">
  51. <span class="flag-icon flag-icon-{{ key[-2:] }}"></span>{{ val }}
  52. </a>
  53. </li>
  54. {% endfor %}
  55. </ul>
  56. </li>
  57. {% endif %}
  58. {% if mailcow_cc_role %}
  59. <li class="nav-item dropdown">
  60. <a href="#" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" role="button" aria-expanded="false">{{ lang.header.mailcow_system }}</a>
  61. <ul class="dropdown-menu">
  62. {% if mailcow_cc_role == 'admin' %}
  63. <li><a href="/debug" class="dropdown-item {% if is_uri('debug') %}active{% endif %}">{{ lang.header.debug }}</a></li>
  64. <li><a href="/admin" class="dropdown-item {% if is_uri('admin') %}active{% endif %}">{{ lang.header.mailcow_config }}</a></li>
  65. {% endif %}
  66. {% if mailcow_cc_role != 'admin' %}
  67. <li><a href="/user" class="dropdown-item {% if is_uri('user') %}active{% endif %}">{{ lang.header.user_settings }}</a></li>
  68. {% endif %}
  69. </ul>
  70. </li>
  71. <li class="nav-item dropdown">
  72. <a href="#" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" role="button" aria-expanded="false">{{ lang.header.email }}</a>
  73. <ul class="dropdown-menu">
  74. {% if mailcow_cc_role == 'admin' or mailcow_cc_role == 'domainadmin' %}
  75. <li><a href="/mailbox" class="dropdown-item {% if is_uri('mailbox') %}active{% endif %}">{{ lang.header.mailcow_config }}</a></li>
  76. {% endif %}
  77. <li><a href="/quarantine" class="dropdown-item {% if is_uri('quarantine') %}active{% endif %}">{{ lang.header.quarantine }}</a></li>
  78. {% if mailcow_cc_role == 'admin' %}
  79. <li><a href="/queue" class="dropdown-item {% if is_uri('queue') %}active{% endif %}">{{ lang.queue.queue_manager }}</a></li>
  80. {% endif %}
  81. <li><a href="#" class="dropdown-item" data-bs-toggle="modal" data-container="sogo-mailcow" data-bs-target="#RestartContainer">{{ lang.header.restart_sogo }}</a></li>
  82. </ul>
  83. </li>
  84. {% endif %}
  85. {% if mailcow_apps or app_links %}
  86. <li class="nav-item dropdown">
  87. <a href="#" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" role="button" aria-expanded="false"><i class="bi bi-link-45deg me-2"></i> {{ ui_texts.apps_name|raw }}</a>
  88. <ul class="dropdown-menu">
  89. {% for app in mailcow_apps %}
  90. {% if not skip_sogo or not is_uri('SOGo', app.link) %}
  91. <li {% if app.description %}title="{{ app.description }}"{% endif %}>
  92. <a href="{{ app.link }}" class="dropdown-item">{{ app.name }}</a>
  93. </li>
  94. {% endif %}
  95. {% endfor %}
  96. {% for row in app_links %}
  97. {% for key, val in row %}
  98. <li><a href="{{ val }}" class="dropdown-item">{{ key }}</a></li>
  99. {% endfor %}
  100. {% endfor %}
  101. </ul>
  102. </li>
  103. {% endif %}
  104. {% if not dual_login and mailcow_cc_username %}
  105. <li class="logged-in-as nav-item"><a href="#" onclick="logout.submit()" class="nav-link"><b class="username-lia">{{ mailcow_cc_username }}</b> <i class="bi bi-power ms-2"></i></a></li>
  106. {% elseif dual_login %}
  107. <li class="logged-in-as nav-item"><a href="#" onclick="logout.submit()" class="nav-link"><b class="username-lia">{{ mailcow_cc_username }} <span class="text-info">({{ dual_login.username }})</span> </b><i class="bi bi-power ms-2"></i></a></li>
  108. {% endif %}
  109. {% if not is_master %}
  110. <li class="text-warning slave-info nav-item">[ slave ]</li>
  111. {% endif %}
  112. </ul>
  113. </div><!--/.nav-collapse -->
  114. </div><!--/.container-fluid -->
  115. </nav>
  116. {% endblock navbar %}
  117. <form action="/" method="post" id="logout"><input type="hidden" name="logout"></form>
  118. {% if ui_texts.ui_announcement_text and ui_texts.ui_announcement_active and not is_root_uri %}
  119. <div class="container mt-4">
  120. <div class="alert alert-{{ ui_texts.ui_announcement_type }}">{{ ui_texts.ui_announcement_text }}</div>
  121. </div>
  122. {% endif %}
  123. <div class="container my-4">
  124. {% block content %}{% endblock %}
  125. </div>
  126. {% include 'modals/footer.twig' %}
  127. <script src="{{ js_path }}"></script>
  128. <script>
  129. var lang_footer = {{ lang_footer|raw }};
  130. var lang_acl = {{ lang_acl|raw }};
  131. var lang_tfa = {{ lang_tfa|raw }};
  132. var lang_fido2 = {{ lang_fido2|raw }};
  133. var docker_timeout = {{ docker_timeout|raw }} * 1000;
  134. var mailcow_cc_role = '{{ mailcow_cc_role }}';
  135. var last_login = '{{ last_login }}';
  136. var mailcow_info = {
  137. version_tag: '{{ mailcow_info.version_tag }}',
  138. last_version_tag: '{{ mailcow_info.last_version_tag }}',
  139. updatedAt: '{{ mailcow_info.updated_at }}',
  140. project_url: '{{ mailcow_info.git_project_url }}',
  141. project_owner: '{{ mailcow_info.git_owner }}',
  142. project_repo: '{{ mailcow_info.git_repo }}',
  143. branch: '{{ mailcow_info.mailcow_branch }}'
  144. };
  145. $(window).scroll(function() {
  146. sessionStorage.scrollTop = $(this).scrollTop();
  147. });
  148. // Select language and reopen active URL without POST
  149. function setLang(sel) {
  150. $.post( '{{ uri }}', {lang: sel} );
  151. window.location.href = window.location.pathname + window.location.search;
  152. }
  153. // FIDO2 functions
  154. function arrayBufferToBase64(buffer) {
  155. let binary = '';
  156. let bytes = new Uint8Array(buffer);
  157. let len = bytes.byteLength;
  158. for (let i = 0; i < len; i++) {
  159. binary += String.fromCharCode( bytes[ i ] );
  160. }
  161. return window.btoa(binary);
  162. }
  163. function recursiveBase64StrToArrayBuffer(obj) {
  164. let prefix = '=?BINARY?B?';
  165. let suffix = '?=';
  166. if (typeof obj === 'object') {
  167. for (let key in obj) {
  168. if (typeof obj[key] === 'string') {
  169. let str = obj[key];
  170. if (str.substring(0, prefix.length) === prefix && str.substring(str.length - suffix.length) === suffix) {
  171. str = str.substring(prefix.length, str.length - suffix.length);
  172. let binary_string = window.atob(str);
  173. let len = binary_string.length;
  174. let bytes = new Uint8Array(len);
  175. for (let i = 0; i < len; i++) {
  176. bytes[i] = binary_string.charCodeAt(i);
  177. }
  178. obj[key] = bytes.buffer;
  179. }
  180. } else {
  181. recursiveBase64StrToArrayBuffer(obj[key]);
  182. }
  183. }
  184. }
  185. }
  186. $(window).on('load', function() {
  187. $(".overlay").hide();
  188. });
  189. $(document).ready(function() {
  190. $(document).on('shown.bs.modal', function(e) {
  191. modal_id = $(e.relatedTarget).data('target');
  192. $(modal_id).attr("aria-hidden","false");
  193. });
  194. // TFA, CSRF, Alerts in footer.inc.php
  195. // Other general functions in mailcow.js
  196. {% for alert_type, alert_msg in alerts %}
  197. mailcow_alert_box('{{ alert_msg|raw|e("js") }}', '{{ alert_type }}');
  198. {% endfor %}
  199. // Confirm TFA modal
  200. {% if pending_tfa_methods %}
  201. new bootstrap.Modal(document.getElementById("ConfirmTFAModal"), {
  202. backdrop: 'static',
  203. keyboard: false
  204. }).show();
  205. // validate Time based OTP tfa
  206. $("#pending_tfa_tab_totp").click(function(){
  207. $(".webauthn-authenticator-selection").removeClass("active");
  208. $("#collapseWebAuthnTFA").collapse('hide');
  209. // select default if only one authenticator exists
  210. if ($('.totp-authenticator-selection').length == 1){
  211. $('.totp-authenticator-selection').addClass("active");
  212. var id = $('.totp-authenticator-selection').children('input').first().val();
  213. $("#totp_selected_id").val(id);
  214. $("#collapseTotpTFA").collapse('show');
  215. }
  216. });
  217. $(".totp-authenticator-selection").click(function(){
  218. $(".totp-authenticator-selection").removeClass("active");
  219. $(this).addClass("active");
  220. var id = $(this).children('input').first().val();
  221. $("#totp_selected_id").val(id);
  222. $("#collapseTotpTFA").collapse('show');
  223. });
  224. if ($('.totp-authenticator-selection').length == 1 &&
  225. $('#pending_tfa_tab_yubi_otp').length == 0 &&
  226. $('.webauthn-authenticator-selection').length == 0){
  227. // select default if only one authenticator exists
  228. $('.totp-authenticator-selection').addClass("active");
  229. var id = $('.totp-authenticator-selection').children('input').first().val();
  230. $("#totp_selected_id").val(id);
  231. $("#collapseTotpTFA").collapse('show');
  232. setTimeout(function() { $("#collapseTotpTFA").find('input[name="token"]').focus(); }, 1000);
  233. }
  234. $('#pending_tfa_tab_totp').on('shown.bs.tab', function() {
  235. // autofocus
  236. setTimeout(function() { $("#collapseTotpTFA").find('input[name="token"]').focus(); }, 200);
  237. });
  238. // validate Yubi OTP tfa
  239. if ($('.webauthn-authenticator-selection').length == 0){
  240. // autofocus
  241. setTimeout(function() { $("#collapseYubiTFA").find('input[name="token"]').focus(); }, 1000);
  242. }
  243. $('#pending_tfa_tab_yubi_otp').on('shown.bs.tab', function() {
  244. // autofocus
  245. $("#collapseYubiTFA").find('input[name="token"]').focus();
  246. });
  247. // validate WebAuthn tfa
  248. $("#pending_tfa_tab_webauthn").click(function(){
  249. $(".totp-authenticator-selection").removeClass("active");
  250. $("#collapseTotpTFA").collapse('hide');
  251. });
  252. $(".webauthn-authenticator-selection").click(function(){
  253. $(".webauthn-authenticator-selection").removeClass("active");
  254. $(this).addClass("active");
  255. var id = $(this).children('input').first().val();
  256. $("#webauthn_selected_id").val(id);
  257. var webauthn_status_auth = document.getElementById('webauthn_status_auth');
  258. webauthn_status_auth.style.setProperty('display', 'flex', 'important');
  259. var webauthn_return_code = document.getElementById('webauthn_return_code');
  260. webauthn_return_code.style.setProperty('display', 'none', 'important');
  261. $("#collapseWebAuthnTFA").collapse('show');
  262. $(this).find('input[name=token]').focus();
  263. if(document.getElementById("webauthn_auth_data") !== null) {
  264. // Check Browser support
  265. if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
  266. window.alert('Browser not supported for WebAuthn.');
  267. return;
  268. }
  269. // fetch webauthn auth args
  270. window.fetch("/api/v1/get/webauthn-tfa-get-args", {method:'GET',cache:'no-cache'}).then(response => {
  271. return response.json();
  272. }).then(json => {
  273. console.log(json);
  274. if (json.success === false) throw new Error();
  275. if (json.type === "error") throw new Error(json.msg);
  276. recursiveBase64StrToArrayBuffer(json);
  277. return json;
  278. }).then(getCredentialArgs => {
  279. // get credentials
  280. return navigator.credentials.get(getCredentialArgs);
  281. }).then(cred => {
  282. return {
  283. id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null,
  284. clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
  285. authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null,
  286. signature : cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null
  287. };
  288. }).then(JSON.stringify).then(function(AuthenticatorAttestationResponse) {
  289. // send request by submit
  290. var form = document.getElementById('webauthn_auth_form');
  291. var auth = document.getElementById('webauthn_auth_data');
  292. auth.value = AuthenticatorAttestationResponse;
  293. form.submit();
  294. }).catch(function(err) {
  295. var webauthn_status_auth = document.getElementById('webauthn_status_auth');
  296. webauthn_status_auth.style.setProperty('display', 'none', 'important');
  297. var webauthn_return_code = document.getElementById('webauthn_return_code');
  298. webauthn_return_code.style.setProperty('display', 'block', 'important');
  299. webauthn_return_code.innerHTML = lang_tfa.error_code + ': ' + err + ' ' + lang_tfa.reload_retry;
  300. });
  301. }
  302. });
  303. $('#ConfirmTFAModal').on('hidden.bs.modal', function(){
  304. // cancel pending login
  305. $.ajax({
  306. type: "GET",
  307. cache: false,
  308. dataType: 'script',
  309. url: '/inc/ajax/destroy_tfa_auth.php',
  310. complete: function(data){
  311. window.location = window.location.href.split("#")[0];
  312. }
  313. });
  314. });
  315. {% endif %}
  316. // Validate FIDO2
  317. $("#fido2-login").click(function(){
  318. $('#fido2-alerts').html();
  319. if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
  320. window.alert('Browser not supported.');
  321. return;
  322. }
  323. window.fetch("/api/v1/get/fido2-get-args", {method:'GET',cache:'no-cache'}).then(function(response) {
  324. return response.json();
  325. }).then(function(json) {
  326. if (json.success === false) {
  327. throw new Error();
  328. }
  329. recursiveBase64StrToArrayBuffer(json);
  330. return json;
  331. }).then(function(getCredentialArgs) {
  332. return navigator.credentials.get(getCredentialArgs);
  333. }).then(function(cred) {
  334. return {
  335. id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null,
  336. clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
  337. authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null,
  338. signature : cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null
  339. };
  340. }).then(JSON.stringify).then(function(AuthenticatorAttestationResponse) {
  341. return window.fetch("/api/v1/process/fido2-args", {method:'POST', body: AuthenticatorAttestationResponse, cache:'no-cache'});
  342. }).then(function(response) {
  343. return response.json();
  344. }).then(function(json) {
  345. if (json.success) {
  346. window.location = window.location.href.split("#")[0];
  347. } else {
  348. throw new Error();
  349. }
  350. }).catch(function(err) {
  351. if (typeof err.message === 'undefined') {
  352. mailcow_alert_box(lang_fido2.fido2_validation_failed, "danger");
  353. } else {
  354. mailcow_alert_box(lang_fido2.fido2_validation_failed + ":<br><i>" + err.message + "</i>", "danger");
  355. }
  356. });
  357. });
  358. // Set TFA/FIDO2
  359. $("#register-fido2, #register-fido2-touchid").click(function(){
  360. let t = $(this);
  361. $("option:selected").prop("selected", false);
  362. if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
  363. window.alert('Browser not supported.');
  364. return;
  365. }
  366. window.fetch("/api/v1/get/fido2-registration/{{ mailcow_cc_username|url_encode(true)|default('null') }}", {method:'GET',cache:'no-cache'}).then(function(response) {
  367. return response.json();
  368. }).then(function(json) {
  369. if (json.success === false) {
  370. throw new Error(json.msg);
  371. }
  372. recursiveBase64StrToArrayBuffer(json);
  373. // set attestation to node if we are registering apple touch id
  374. if(t.attr('id') === 'register-fido2-touchid') {
  375. json.publicKey.attestation = 'none';
  376. json.publicKey.authenticatorSelection.authenticatorAttachment = "platform";
  377. }
  378. return json;
  379. }).then(function(createCredentialArgs) {
  380. console.log(createCredentialArgs);
  381. return navigator.credentials.create(createCredentialArgs);
  382. }).then(function(cred) {
  383. return {
  384. clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
  385. attestationObject: cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null
  386. };
  387. }).then(JSON.stringify).then(function(AuthenticatorAttestationResponse) {
  388. return window.fetch("/api/v1/add/fido2-registration", {method:'POST', body: AuthenticatorAttestationResponse, cache:'no-cache'});
  389. }).then(function(response) {
  390. return response.json();
  391. }).then(function(json) {
  392. if (json.success) {
  393. window.location = window.location.href.split("#")[0];
  394. } else {
  395. throw new Error(json.msg);
  396. }
  397. }).catch(function(err) {
  398. $('#fido2-alerts').html('<span class="text-danger"><b>' + err.message + '</b></span>');
  399. });
  400. });
  401. $('#selectTFA').change(function () {
  402. if ($(this).val() == "yubi_otp") {
  403. $('#YubiOTPModal').modal('show');
  404. $("option:selected").prop("selected", false);
  405. }
  406. if ($(this).val() == "totp") {
  407. $('#TOTPModal').modal('show');
  408. request_token = $('#tfa-qr-img').data('totp-secret');
  409. $.ajax({
  410. url: '/inc/ajax/qr_gen.php',
  411. data: {
  412. token: request_token,
  413. },
  414. }).done(function (result) {
  415. $("#tfa-qr-img").attr("src", result);
  416. });
  417. $("option:selected").prop("selected", false);
  418. }
  419. if ($(this).val() == "webauthn") {
  420. // check if Browser is supported
  421. if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
  422. window.alert('Browser not supported.');
  423. return;
  424. }
  425. // show modal
  426. $('#WebAuthnModal').modal('show');
  427. $("option:selected").prop("selected", false);
  428. $("#start_webauthn_register").click(() => {
  429. var key_id = document.getElementsByName('key_id')[1].value;
  430. var confirm_password = document.getElementsByName('confirm_password')[1].value;
  431. // fetch WebAuthn create args
  432. window.fetch("/api/v1/get/webauthn-tfa-registration/{{ mailcow_cc_username|url_encode(true)|default('null') }}", {method:'GET',cache:'no-cache'}).then(response => {
  433. return response.json();
  434. }).then(json => {
  435. console.log(json);
  436. if (json.success === false) throw new Error(json.msg);
  437. recursiveBase64StrToArrayBuffer(json);
  438. return json;
  439. }).then(createCredentialArgs => {
  440. // create credentials
  441. return navigator.credentials.create(createCredentialArgs);
  442. }).then(cred => {
  443. return {
  444. clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
  445. attestationObject: cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null,
  446. key_id: key_id,
  447. tfa_method: "webauthn",
  448. confirm_password: confirm_password
  449. };
  450. }).then(JSON.stringify).then(AuthenticatorAttestationResponse => {
  451. // send request
  452. return window.fetch("/api/v1/add/webauthn-tfa-registration", {method:'POST', body: AuthenticatorAttestationResponse, cache:'no-cache'});
  453. }).then(response => {
  454. return response.json();
  455. }).then(json => {
  456. if (json.success) {
  457. // reload on success
  458. window.location = window.location.href.split("#")[0];
  459. } else {
  460. throw new Error(json.msg);
  461. }
  462. }).catch(function(err) {
  463. console.log(err);
  464. var webauthn_return_code = document.getElementById('webauthn_return_code');
  465. webauthn_return_code.style.display = webauthn_return_code.style.display === 'none' ? '' : null;
  466. webauthn_return_code.innerHTML = lang_tfa.error_code + ': ' + err + ' ' + lang_tfa.reload_retry;
  467. });
  468. });
  469. }
  470. if ($(this).val() == "none") {
  471. $('#DisableTFAModal').modal('show');
  472. $("option:selected").prop("selected", false);
  473. }
  474. });
  475. {% if mailcow_cc_username %}
  476. // Reload after session timeout
  477. var session_lifetime = {{ (session_lifetime * 1000) + 15000 }};
  478. setTimeout(function() {
  479. location.reload();
  480. }, session_lifetime);
  481. {% endif %}
  482. // CSRF
  483. $('<input type="hidden" value="{{ csrf_token }}">').attr('name', 'csrf_token').appendTo('form');
  484. if (sessionStorage.scrollTop != "undefined") {
  485. $(window).scrollTop(sessionStorage.scrollTop);
  486. }
  487. });
  488. </script>
  489. <div class="container footer">
  490. {% if ui_texts.ui_footer %}
  491. <hr><span class="rot-enc">{{ ui_texts.ui_footer|rot13|raw }}</span>
  492. {% endif %}
  493. {% if mailcow_cc_username and mailcow_info.mailcow_branch|lower == "master" and mailcow_info.version_tag|default %}
  494. <span class="version">
  495. 🐮 + 🐋 = 💕
  496. Version: <a href="{{ mailcow_info.git_project_url }}/releases/tag/{{ mailcow_info.version_tag }}" target="_blank">{{ mailcow_info.version_tag }}
  497. </a>
  498. </span>
  499. {% endif %}
  500. {% if mailcow_cc_username and mailcow_info.mailcow_branch|lower == "nightly" and mailcow_info.version_tag|default %}
  501. <span class="version">
  502. 🛠️🐮 + 🐋 = 💕
  503. Nightly: <a href="{{ mailcow_info.git_project_url }}/commit/{{ mailcow_info.git_commit }}" target="_blank">{{ mailcow_info.version_tag }}
  504. </a><br>
  505. <span style="text-align:right;display:block;">Build: {{ mailcow_info.git_commit_date }}</span>
  506. </span>
  507. {% endif %}
  508. </div>
  509. </body>
  510. </html>