base.twig 23 KB

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