UIWindowEmailConfirmationRequired.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. /**
  2. * Copyright (C) 2024 Puter Technologies Inc.
  3. *
  4. * This file is part of Puter.
  5. *
  6. * Puter is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Affero General Public License as published
  8. * by the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU Affero General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Affero General Public License
  17. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  18. */
  19. import UIWindow from './UIWindow.js'
  20. import UIAlert from './UIAlert.js'
  21. function UIWindowEmailConfirmationRequired(options){
  22. return new Promise(async (resolve) => {
  23. options = options ?? {};
  24. let final_code = '';
  25. let is_checking_code = false;
  26. const submit_btn_txt = 'Confirm Email'
  27. let h = '';
  28. h += `<div class="qr-code-window-close-btn generic-close-window-button"> &times; </div>`;
  29. h += `<div style="-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; color: #3e5362;">`;
  30. h += `<img src="${html_encode(window.icons['mail.svg'])}" style="display:block; margin:10px auto 10px;">`;
  31. h += `<h3 style="text-align:center; font-weight: 500; font-size: 20px;">Confirm Your Email Address</h3>`;
  32. h += `<form>`;
  33. h += `<p style="text-align:center; padding: 0 20px;">To continue, please enter the 6-digit confirmation code sent to <strong style="font-weight: 500;">${window.user.email}</strong></p>`;
  34. h += `<div class="error"></div>`;
  35. h += ` <fieldset name="number-code" style="border: none; padding:0;" data-number-code-form>
  36. <input class="digit-input" type="number" min='0' max='9' name='number-code-0' data-number-code-input='0' required />
  37. <input class="digit-input" type="number" min='0' max='9' name='number-code-1' data-number-code-input='1' required />
  38. <input class="digit-input" type="number" min='0' max='9' name='number-code-2' data-number-code-input='2' required />
  39. <span class="email-confirm-code-hyphen">-</span>
  40. <input class="digit-input" type="number" min='0' max='9' name='number-code-3' data-number-code-input='3' required />
  41. <input class="digit-input" type="number" min='0' max='9' name='number-code-4' data-number-code-input='4' required />
  42. <input class="digit-input" type="number" min='0' max='9' name='number-code-5' data-number-code-input='5' required />
  43. </fieldset>`;
  44. h += `<button type="submit" class="button button-block button-primary email-confirm-btn" style="margin-top:10px;" disabled>${submit_btn_txt}</button>`;
  45. h += `</form>`;
  46. h += `<div style="text-align:center; padding:10px; font-size:14px; margin-top:10px;">`;
  47. h += `<span class="send-conf-email">Re-send Confirmation Code</span>`;
  48. if(options.logout_in_footer){
  49. h += ` &bull; `;
  50. h += `<span class="conf-email-log-out">Log Out</span>`;
  51. }
  52. h += `</div>`;
  53. h += `</div>`;
  54. const el_window = await UIWindow({
  55. title: null,
  56. icon: null,
  57. uid: null,
  58. is_dir: false,
  59. body_content: h,
  60. has_head: false,
  61. selectable_body: false,
  62. draggable_body: true,
  63. allow_context_menu: false,
  64. is_draggable: options.is_draggable ?? true,
  65. is_droppable: false,
  66. is_resizable: false,
  67. stay_on_top: options.stay_on_top ?? false,
  68. allow_native_ctxmenu: true,
  69. allow_user_select: true,
  70. backdrop: true,
  71. width: 390,
  72. dominant: true,
  73. onAppend: function(el_window){
  74. $(el_window).find('.digit-input').first().focus();
  75. },
  76. window_class: 'window-item-properties',
  77. window_css:{
  78. height: 'initial',
  79. },
  80. body_css: {
  81. padding: '30px',
  82. width: 'initial',
  83. height: 'initial',
  84. 'background-color': 'rgb(247 251 255)',
  85. 'backdrop-filter': 'blur(3px)',
  86. }
  87. })
  88. $(el_window).find('.digit-input').first().focus();
  89. $(el_window).find('.email-confirm-btn').on('click submit', function(e){
  90. e.preventDefault();
  91. e.stopPropagation();
  92. $(el_window).find('.email-confirm-btn').prop('disabled', true);
  93. $(el_window).find('.error').hide();
  94. // Check if already checking code to prevent multiple requests
  95. if(is_checking_code)
  96. return;
  97. // Confirm button
  98. is_checking_code = true;
  99. // set animation
  100. $(el_window).find('.email-confirm-btn').html(`<svg style="width:20px; margin-top: 5px;" xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24"><title>circle anim</title><g fill="#fff" class="nc-icon-wrapper"><g class="nc-loop-circle-24-icon-f"><path d="M12 24a12 12 0 1 1 12-12 12.013 12.013 0 0 1-12 12zm0-22a10 10 0 1 0 10 10A10.011 10.011 0 0 0 12 2z" fill="#eee" opacity=".4"></path><path d="M24 12h-2A10.011 10.011 0 0 0 12 2V0a12.013 12.013 0 0 1 12 12z" data-color="color-2"></path></g><style>.nc-loop-circle-24-icon-f{--animation-duration:0.5s;transform-origin:12px 12px;animation:nc-loop-circle-anim var(--animation-duration) infinite linear}@keyframes nc-loop-circle-anim{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}</style></g></svg>`);
  101. setTimeout(() => {
  102. $.ajax({
  103. url: api_origin + "/confirm-email",
  104. type: 'POST',
  105. data: JSON.stringify({
  106. code: final_code,
  107. }),
  108. async: true,
  109. contentType: "application/json",
  110. headers: {
  111. "Authorization": "Bearer "+auth_token
  112. },
  113. statusCode: {
  114. 401: function () {
  115. logout();
  116. },
  117. },
  118. success: function (res){
  119. if(res.email_confirmed){
  120. $(el_window).close();
  121. refresh_user_data(window.auth_token)
  122. resolve(true);
  123. }else{
  124. $(el_window).find('.error').html('Invalid confirmation code.');
  125. $(el_window).find('.error').fadeIn();
  126. $(el_window).find('.digit-input').val('');
  127. $(el_window).find('.digit-input').first().focus();
  128. $(el_window).find('.email-confirm-btn').prop('disabled', false);
  129. $(el_window).find('.email-confirm-btn').html(submit_btn_txt);
  130. }
  131. },
  132. error: function(res){
  133. $(el_window).find('.error').html(res.responseJSON.error);
  134. $(el_window).find('.error').fadeIn();
  135. $(el_window).find('.digit-input').val('');
  136. $(el_window).find('.digit-input').first().focus();
  137. $(el_window).find('.email-confirm-btn').prop('disabled', false);
  138. $(el_window).find('.email-confirm-btn').html(submit_btn_txt);
  139. },
  140. complete: function(){
  141. is_checking_code = false;
  142. }
  143. })
  144. }, 1000);
  145. })
  146. // send email confirmation
  147. $(el_window).find('.send-conf-email').on('click', function(e){
  148. $.ajax({
  149. url: api_origin + "/send-confirm-email",
  150. type: 'POST',
  151. async: true,
  152. contentType: "application/json",
  153. headers: {
  154. "Authorization": "Bearer "+auth_token
  155. },
  156. statusCode: {
  157. 401: function () {
  158. logout();
  159. },
  160. },
  161. success: async function (res){
  162. await UIAlert({
  163. message: `A new confirmation code has been sent to <strong>${window.user.email}</strong>.`,
  164. body_icon: window.icons['c-check.svg'],
  165. stay_on_top: true,
  166. backdrop: true,
  167. })
  168. $(el_window).find('.digit-input').first().focus();
  169. },
  170. complete: function(){
  171. }
  172. })
  173. })
  174. // logout
  175. $(el_window).find('.conf-email-log-out').on('click', function(e){
  176. logout();
  177. $(el_window).close();
  178. })
  179. // Elements
  180. const numberCodeForm = document.querySelector('[data-number-code-form]');
  181. const numberCodeInputs = [...numberCodeForm.querySelectorAll('[data-number-code-input]')];
  182. // Event listeners
  183. numberCodeForm.addEventListener('input', ({ target }) => {
  184. if(!target.value.length) { return target.value = null; }
  185. const inputLength = target.value.length;
  186. let currentIndex = Number(target.dataset.numberCodeInput);
  187. if(inputLength === 2){
  188. const inputValues = target.value.split('');
  189. target.value = inputValues[0];
  190. }
  191. else if (inputLength > 1) {
  192. const inputValues = target.value.split('');
  193. inputValues.forEach((value, valueIndex) => {
  194. const nextValueIndex = currentIndex + valueIndex;
  195. if (nextValueIndex >= numberCodeInputs.length) { return; }
  196. numberCodeInputs[nextValueIndex].value = value;
  197. });
  198. currentIndex += inputValues.length - 2;
  199. }
  200. const nextIndex = currentIndex + 1;
  201. if (nextIndex < numberCodeInputs.length) {
  202. numberCodeInputs[nextIndex].focus();
  203. }
  204. // Concatenate all inputs into one string to create the final code
  205. final_code = '';
  206. for(let i=0; i< numberCodeInputs.length; i++){
  207. final_code += numberCodeInputs[i].value;
  208. }
  209. // Automatically submit if 6 digits entered
  210. if(final_code.length === 6){
  211. $(el_window).find('.email-confirm-btn').prop('disabled', false);
  212. $(el_window).find('.email-confirm-btn').trigger('click');
  213. }
  214. });
  215. numberCodeForm.addEventListener('keydown', (e) => {
  216. const { code, target } = e;
  217. const currentIndex = Number(target.dataset.numberCodeInput);
  218. const previousIndex = currentIndex - 1;
  219. const nextIndex = currentIndex + 1;
  220. const hasPreviousIndex = previousIndex >= 0;
  221. const hasNextIndex = nextIndex <= numberCodeInputs.length - 1
  222. switch (code) {
  223. case 'ArrowLeft':
  224. case 'ArrowUp':
  225. if (hasPreviousIndex) {
  226. numberCodeInputs[previousIndex].focus();
  227. }
  228. e.preventDefault();
  229. break;
  230. case 'ArrowRight':
  231. case 'ArrowDown':
  232. if (hasNextIndex) {
  233. numberCodeInputs[nextIndex].focus();
  234. }
  235. e.preventDefault();
  236. break;
  237. case 'Backspace':
  238. if (!e.target.value.length && hasPreviousIndex) {
  239. numberCodeInputs[previousIndex].value = null;
  240. numberCodeInputs[previousIndex].focus();
  241. }
  242. break;
  243. default:
  244. break;
  245. }
  246. });
  247. })
  248. }
  249. export default UIWindowEmailConfirmationRequired