base.twig 25 KB

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