base.twig 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  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 ||
  14. JSON.parse(localStorage.getItem("darkmode")) === true) {
  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 }}"></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 }}"></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_settings }}</a>
  61. <ul class="dropdown-menu">
  62. {% if mailcow_cc_role == 'admin' %}
  63. <li><a href="/admin" class="dropdown-item {% if is_uri('admin') %}active{% endif %}">{{ lang.header.administration }}</a></li>
  64. <li><a href="/debug" class="dropdown-item {% if is_uri('debug') %}active{% endif %}">{{ lang.header.debug }}</a></li>
  65. {% endif %}
  66. {% if mailcow_cc_role == 'admin' or mailcow_cc_role == 'domainadmin' %}
  67. <li><a href="/mailbox" class="dropdown-item {% if is_uri('mailbox') %}active{% endif %}">{{ lang.header.mailboxes }}</a></li>
  68. {% endif %}
  69. {% if mailcow_cc_role != 'admin' %}
  70. <li><a href="/user" class="dropdown-item {% if is_uri('user') %}active{% endif %}">{{ lang.header.user_settings }}</a></li>
  71. {% endif %}
  72. </ul>
  73. </li>
  74. <li class="nav-item"><a href="/quarantine" class="nav-link {% if is_uri('quarantine') %}active{% endif %}"><i class="bi bi-inbox-fill me-2"></i> {{ lang.header.quarantine }}</a></li>
  75. {% endif %}
  76. {% if mailcow_cc_role == 'admin' and not skip_sogo %}
  77. <li class="nav-item"><a href="#" class="nav-link" data-bs-toggle="modal" data-container="sogo-mailcow" data-bs-target="#RestartContainer"><i class="bi bi-arrow-repeat me-2"></i> {{ lang.header.restart_sogo }}</a></li>
  78. {% endif %}
  79. {% if mailcow_apps or app_links %}
  80. <li class="nav-item dropdown">
  81. <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>
  82. <ul class="dropdown-menu">
  83. {% for app in mailcow_apps %}
  84. {% if not skip_sogo or not is_uri('SOGo', app.link) %}
  85. <li {% if app.description %}title="{{ app.description }}"{% endif %}>
  86. <a href="{{ app.link }}" class="dropdown-item">{{ app.name }}</a>
  87. </li>
  88. {% endif %}
  89. {% endfor %}
  90. {% for row in app_links %}
  91. {% for key, val in row %}
  92. <li><a href="{{ val }}" class="dropdown-item">{{ key }}</a></li>
  93. {% endfor %}
  94. {% endfor %}
  95. </ul>
  96. </li>
  97. {% endif %}
  98. {% if not dual_login and mailcow_cc_username %}
  99. <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>
  100. {% elseif dual_login %}
  101. <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>
  102. {% endif %}
  103. {% if not is_master %}
  104. <li class="text-warning slave-info nav-item">[ slave ]</li>
  105. {% endif %}
  106. </ul>
  107. </div><!--/.nav-collapse -->
  108. </div><!--/.container-fluid -->
  109. </nav>
  110. {% endblock navbar %}
  111. <form action="/" method="post" id="logout"><input type="hidden" name="logout"></form>
  112. {% if ui_texts.ui_announcement_text and ui_texts.ui_announcement_active and not is_root_uri %}
  113. <div class="container">
  114. <div class="alert alert-{{ ui_texts.ui_announcement_type }}">{{ ui_texts.ui_announcement_text }}</div>
  115. </div>
  116. {% endif %}
  117. <div class="container my-4">
  118. {% block content %}{% endblock %}
  119. </div>
  120. {% include 'modals/footer.twig' %}
  121. <script src="{{ js_path }}"></script>
  122. <script>
  123. var lang_footer = {{ lang_footer|raw }};
  124. var lang_acl = {{ lang_acl|raw }};
  125. var lang_tfa = {{ lang_tfa|raw }};
  126. var lang_fido2 = {{ lang_fido2|raw }};
  127. var docker_timeout = {{ docker_timeout|raw }} * 1000;
  128. $(window).scroll(function() {
  129. sessionStorage.scrollTop = $(this).scrollTop();
  130. });
  131. // Select language and reopen active URL without POST
  132. function setLang(sel) {
  133. $.post( '{{ uri }}', {lang: sel} );
  134. window.location.href = window.location.pathname + window.location.search;
  135. }
  136. // FIDO2 functions
  137. function arrayBufferToBase64(buffer) {
  138. let binary = '';
  139. let bytes = new Uint8Array(buffer);
  140. let len = bytes.byteLength;
  141. for (let i = 0; i < len; i++) {
  142. binary += String.fromCharCode( bytes[ i ] );
  143. }
  144. return window.btoa(binary);
  145. }
  146. function recursiveBase64StrToArrayBuffer(obj) {
  147. let prefix = '=?BINARY?B?';
  148. let suffix = '?=';
  149. if (typeof obj === 'object') {
  150. for (let key in obj) {
  151. if (typeof obj[key] === 'string') {
  152. let str = obj[key];
  153. if (str.substring(0, prefix.length) === prefix && str.substring(str.length - suffix.length) === suffix) {
  154. str = str.substring(prefix.length, str.length - suffix.length);
  155. let binary_string = window.atob(str);
  156. let len = binary_string.length;
  157. let bytes = new Uint8Array(len);
  158. for (let i = 0; i < len; i++) {
  159. bytes[i] = binary_string.charCodeAt(i);
  160. }
  161. obj[key] = bytes.buffer;
  162. }
  163. } else {
  164. recursiveBase64StrToArrayBuffer(obj[key]);
  165. }
  166. }
  167. }
  168. }
  169. $(window).on('load', function() {
  170. $(".overlay").hide();
  171. });
  172. $(document).ready(function() {
  173. $(document).on('shown.bs.modal', function(e) {
  174. modal_id = $(e.relatedTarget).data('target');
  175. $(modal_id).attr("aria-hidden","false");
  176. });
  177. // TFA, CSRF, Alerts in footer.inc.php
  178. // Other general functions in mailcow.js
  179. {% for alert_type, alert_msg in alerts %}
  180. mailcow_alert_box('{{ alert_msg|raw }}', '{{ alert_type }}');
  181. {% endfor %}
  182. // Confirm TFA modal
  183. {% if pending_tfa_method %}
  184. new bootstrap.Modal(document.getElementById("ConfirmTFAModal"), {
  185. backdrop: 'static',
  186. keyboard: false
  187. }).show();
  188. // validate WebAuthn tfa
  189. $('#start_webauthn_confirmation').click(function(){
  190. $('#webauthn_status_auth').html('<p><i class="bi bi-arrow-repeat icon-spin"></i> ' + lang_tfa.init_webauthn + '</p>');
  191. $(this).find('input[name=token]').focus();
  192. if(document.getElementById("webauthn_auth_data") !== null) {
  193. // Check Browser support
  194. if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
  195. window.alert('Browser not supported for WebAuthn.');
  196. return;
  197. }
  198. // fetch webauthn auth args
  199. window.fetch("/api/v1/get/webauthn-tfa-get-args", {method:'GET',cache:'no-cache'}).then(response => {
  200. return response.json();
  201. }).then(json => {
  202. if (json.success === false) throw new Error();
  203. recursiveBase64StrToArrayBuffer(json);
  204. return json;
  205. }).then(getCredentialArgs => {
  206. // get credentials
  207. return navigator.credentials.get(getCredentialArgs);
  208. }).then(cred => {
  209. return {
  210. id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null,
  211. clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
  212. authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null,
  213. signature : cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null
  214. };
  215. }).then(JSON.stringify).then(function(AuthenticatorAttestationResponse) {
  216. // send request by submit
  217. var form = document.getElementById('webauthn_auth_form');
  218. var auth = document.getElementById('webauthn_auth_data');
  219. auth.value = AuthenticatorAttestationResponse;
  220. form.submit();
  221. }).catch(function(err) {
  222. var webauthn_return_code = document.getElementById('webauthn_return_code');
  223. webauthn_return_code.style.display = webauthn_return_code.style.display === 'none' ? '' : null;
  224. webauthn_return_code.innerHTML = lang_tfa.error_code + ': ' + err + ' ' + lang_tfa.reload_retry;
  225. });
  226. }
  227. });
  228. $('#ConfirmTFAModal').on('hidden.bs.modal', function(){
  229. // cancel pending login
  230. $.ajax({
  231. type: "GET",
  232. cache: false,
  233. dataType: 'script',
  234. url: '/inc/ajax/destroy_tfa_auth.php',
  235. complete: function(data){
  236. window.location = window.location.href.split("#")[0];
  237. }
  238. });
  239. });
  240. {% endif %}
  241. // Validate FIDO2
  242. $("#fido2-login").click(function(){
  243. $('#fido2-alerts').html();
  244. if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
  245. window.alert('Browser not supported.');
  246. return;
  247. }
  248. window.fetch("/api/v1/get/fido2-get-args", {method:'GET',cache:'no-cache'}).then(function(response) {
  249. return response.json();
  250. }).then(function(json) {
  251. if (json.success === false) {
  252. throw new Error();
  253. }
  254. recursiveBase64StrToArrayBuffer(json);
  255. return json;
  256. }).then(function(getCredentialArgs) {
  257. return navigator.credentials.get(getCredentialArgs);
  258. }).then(function(cred) {
  259. return {
  260. id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null,
  261. clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
  262. authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null,
  263. signature : cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null
  264. };
  265. }).then(JSON.stringify).then(function(AuthenticatorAttestationResponse) {
  266. return window.fetch("/api/v1/process/fido2-args", {method:'POST', body: AuthenticatorAttestationResponse, cache:'no-cache'});
  267. }).then(function(response) {
  268. return response.json();
  269. }).then(function(json) {
  270. if (json.success) {
  271. window.location = window.location.href.split("#")[0];
  272. } else {
  273. throw new Error();
  274. }
  275. }).catch(function(err) {
  276. if (typeof err.message === 'undefined') {
  277. mailcow_alert_box(lang_fido2.fido2_validation_failed, "danger");
  278. } else {
  279. mailcow_alert_box(lang_fido2.fido2_validation_failed + ":<br><i>" + err.message + "</i>", "danger");
  280. }
  281. });
  282. });
  283. // Set TFA/FIDO2
  284. $("#register-fido2, #register-fido2-touchid").click(function(){
  285. let t = $(this);
  286. $("option:selected").prop("selected", false);
  287. if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
  288. window.alert('Browser not supported.');
  289. return;
  290. }
  291. window.fetch("/api/v1/get/fido2-registration/{{ mailcow_cc_username|url_encode(true)|default('null') }}", {method:'GET',cache:'no-cache'}).then(function(response) {
  292. return response.json();
  293. }).then(function(json) {
  294. if (json.success === false) {
  295. throw new Error(json.msg);
  296. }
  297. recursiveBase64StrToArrayBuffer(json);
  298. // set attestation to node if we are registering apple touch id
  299. if(t.attr('id') === 'register-fido2-touchid') {
  300. json.publicKey.attestation = 'none';
  301. json.publicKey.authenticatorSelection.authenticatorAttachment = "platform";
  302. }
  303. return json;
  304. }).then(function(createCredentialArgs) {
  305. console.log(createCredentialArgs);
  306. return navigator.credentials.create(createCredentialArgs);
  307. }).then(function(cred) {
  308. return {
  309. clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
  310. attestationObject: cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null
  311. };
  312. }).then(JSON.stringify).then(function(AuthenticatorAttestationResponse) {
  313. return window.fetch("/api/v1/add/fido2-registration", {method:'POST', body: AuthenticatorAttestationResponse, cache:'no-cache'});
  314. }).then(function(response) {
  315. return response.json();
  316. }).then(function(json) {
  317. if (json.success) {
  318. window.location = window.location.href.split("#")[0];
  319. } else {
  320. throw new Error(json.msg);
  321. }
  322. }).catch(function(err) {
  323. $('#fido2-alerts').html('<span class="text-danger"><b>' + err.message + '</b></span>');
  324. });
  325. });
  326. $('#selectTFA').change(function () {
  327. if ($(this).val() == "yubi_otp") {
  328. $('#YubiOTPModal').modal('show');
  329. $("option:selected").prop("selected", false);
  330. }
  331. if ($(this).val() == "totp") {
  332. $('#TOTPModal').modal('show');
  333. request_token = $('#tfa-qr-img').data('totp-secret');
  334. $.ajax({
  335. url: '/inc/ajax/qr_gen.php',
  336. data: {
  337. token: request_token,
  338. },
  339. }).done(function (result) {
  340. $("#tfa-qr-img").attr("src", result);
  341. });
  342. $("option:selected").prop("selected", false);
  343. }
  344. if ($(this).val() == "webauthn") {
  345. // check if Browser is supported
  346. if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
  347. window.alert('Browser not supported.');
  348. return;
  349. }
  350. // show modal
  351. $('#WebAuthnModal').modal('show');
  352. $("option:selected").prop("selected", false);
  353. $("#start_webauthn_register").click(() => {
  354. var key_id = document.getElementsByName('key_id')[1].value;
  355. // fetch WebAuthn create args
  356. window.fetch("/api/v1/get/webauthn-tfa-registration/{{ mailcow_cc_username|url_encode(true)|default('null') }}", {method:'GET',cache:'no-cache'}).then(response => {
  357. return response.json();
  358. }).then(json => {
  359. if (json.success === false) throw new Error(json.msg);
  360. recursiveBase64StrToArrayBuffer(json);
  361. return json;
  362. }).then(createCredentialArgs => {
  363. // create credentials
  364. return navigator.credentials.create(createCredentialArgs);
  365. }).then(cred => {
  366. return {
  367. clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
  368. attestationObject: cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null,
  369. key_id: key_id,
  370. tfa_method: "webauthn"
  371. };
  372. }).then(JSON.stringify).then(AuthenticatorAttestationResponse => {
  373. // send request
  374. return window.fetch("/api/v1/add/webauthn-tfa-registration", {method:'POST', body: AuthenticatorAttestationResponse, cache:'no-cache'});
  375. }).then(response => {
  376. return response.json();
  377. }).then(json => {
  378. if (json.success) {
  379. // reload on success
  380. window.location = window.location.href.split("#")[0];
  381. } else {
  382. throw new Error(json.msg);
  383. }
  384. }).catch(function(err) {
  385. console.log(err);
  386. var webauthn_return_code = document.getElementById('webauthn_return_code');
  387. webauthn_return_code.style.display = webauthn_return_code.style.display === 'none' ? '' : null;
  388. webauthn_return_code.innerHTML = lang_tfa.error_code + ': ' + err + ' ' + lang_tfa.reload_retry;
  389. });
  390. });
  391. }
  392. if ($(this).val() == "none") {
  393. $('#DisableTFAModal').modal('show');
  394. $("option:selected").prop("selected", false);
  395. }
  396. });
  397. {% if mailcow_cc_username %}
  398. // Reload after session timeout
  399. var session_lifetime = {{ (session_lifetime * 1000) + 15000 }};
  400. setTimeout(function() {
  401. location.reload();
  402. }, session_lifetime);
  403. {% endif %}
  404. // CSRF
  405. $('<input type="hidden" value="{{ csrf_token }}">').attr('name', 'csrf_token').appendTo('form');
  406. if (sessionStorage.scrollTop != "undefined") {
  407. $(window).scrollTop(sessionStorage.scrollTop);
  408. }
  409. });
  410. </script>
  411. <div class="container footer">
  412. {% if ui_texts.ui_footer %}
  413. <hr><span class="rot-enc">{{ ui_texts.ui_footer|rot13|raw }}</span>
  414. {% endif %}
  415. {% if mailcow_cc_username and mailcow_info.version_tag|default %}
  416. <span class="version">
  417. 🐮 + 🐋 = 💕
  418. <a href="{{ mailcow_info.git_project_url }}/releases/tag/{{ mailcow_info.version_tag }}" target="_blank">
  419. Version: {{ mailcow_info.version_tag }}
  420. </a>
  421. </span>
  422. {% endif %}
  423. </div>
  424. </body>
  425. </html>