main.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  2. const Cookies = {
  3. /**
  4. * Creates a cookie.
  5. *
  6. * @param {string} name The name of the cookie.
  7. * @param {any} value The value to assign the cookie. It will be JSON encoded using JSON.stringify(...).
  8. * @param {number} days The number of days in which the cookie will expire. If none is provided,
  9. * it will create a session cookie.
  10. */
  11. set(name, value, days = null) {
  12. let expires = '';
  13. if (days && !isNaN(days)) {
  14. const date = new Date();
  15. date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
  16. expires = `; expires=${date.toUTCString()}`;
  17. }
  18. document.cookie =
  19. `${name}=${JSON.stringify(value)}` + expires + '; path="/"; SameSite=None; Secure';
  20. },
  21. /**
  22. * Reads a cookie.
  23. *
  24. * @param {string} name The name of the cookie.
  25. * @returns {string} The value of the cookie, decoded with JSON.parse(...).
  26. */
  27. read(name) {
  28. const value = document.cookie
  29. .split('; ')
  30. .find((row) => row.startsWith(`${name}=`))
  31. ?.split('=')[1];
  32. return value ? JSON.parse(value) : undefined;
  33. },
  34. /**
  35. * Removes a cookie.
  36. *
  37. * @param {string} name The name of the cookie.
  38. */
  39. remove(name) {
  40. this.set(name, '', -1);
  41. },
  42. };
  43. /**
  44. * generates a random string using a cryptographically secure rng,
  45. * and ensuring it contains at least 1 lowercase, 1 uppercase, and 1 number.
  46. *
  47. * @param {int} [length=16]
  48. * @throws {Error} if length is too small to create a "sufficiently secure" string
  49. * @returns {string}
  50. */
  51. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  52. function randomString(length = 16) {
  53. const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
  54. const rng = (min, max) => {
  55. if (min < 0 || min > 0xffff) {
  56. throw new Error(
  57. 'minimum supported number is 0, this generator can only make numbers between 0-65535 inclusive.'
  58. );
  59. }
  60. if (max > 0xffff || max < 0) {
  61. throw new Error(
  62. 'max supported number is 65535, this generator can only make numbers between 0-65535 inclusive.'
  63. );
  64. }
  65. if (min > max) {
  66. throw new Error('dude min>max wtf');
  67. }
  68. // micro-optimization
  69. const randArr = max > 255 ? new Uint16Array(1) : new Uint8Array(1);
  70. let result;
  71. let attempts = 0;
  72. // eslint-disable-next-line no-constant-condition
  73. while (true) {
  74. crypto.getRandomValues(randArr);
  75. result = randArr[0];
  76. if (result >= min && result <= max) {
  77. return result;
  78. }
  79. ++attempts;
  80. if (attempts > 1000000) {
  81. // should basically never happen with max 0xFFFF/Uint16Array.
  82. throw new Error('tried a million times, something is wrong');
  83. }
  84. }
  85. };
  86. let attempts = 0;
  87. const minimumStrengthRegex = new RegExp(
  88. /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*\d)[a-zA-Z\d]{8,}$/
  89. );
  90. const randmax = chars.length - 1;
  91. // eslint-disable-next-line no-constant-condition
  92. while (true) {
  93. let result = '';
  94. for (let i = 0; i < length; ++i) {
  95. result += chars[rng(0, randmax)];
  96. }
  97. if (minimumStrengthRegex.test(result)) {
  98. return result;
  99. }
  100. ++attempts;
  101. if (attempts > 1000000) {
  102. throw new Error('tried a million times, something is wrong');
  103. }
  104. }
  105. }
  106. document.addEventListener('alpine:init', () => {
  107. // Sticky class helper
  108. window.addEventListener('scroll', () => {
  109. const toolbar = document.querySelector('.toolbar');
  110. const tableHeader = document.querySelector('.table-header');
  111. const toolbarOffset =
  112. toolbar.getBoundingClientRect().top + (window.scrollY - document.documentElement.clientTop);
  113. const headerHeight = document.querySelector('.top-bar').offsetHeight;
  114. const isActive = window.scrollY > toolbarOffset - headerHeight;
  115. toolbar.classList.toggle('active', isActive);
  116. if (tableHeader) {
  117. tableHeader.classList.toggle('active', isActive);
  118. }
  119. });
  120. // Select all helper
  121. const toggleAll = document.querySelector('#toggle-all');
  122. if (toggleAll) {
  123. toggleAll.addEventListener('change', (evt) => {
  124. document.querySelectorAll('.ch-toggle').forEach((el) => (el.checked = evt.target.checked));
  125. document
  126. .querySelectorAll('.l-unit')
  127. .forEach((el) => el.classList.toggle('selected', evt.target.checked));
  128. });
  129. }
  130. // Bulk edit forms
  131. Alpine.bind('BulkEdit', () => ({
  132. /** @param {SubmitEvent} evt */
  133. '@submit'(evt) {
  134. evt.preventDefault();
  135. document.querySelectorAll('.ch-toggle').forEach((el) => {
  136. if (el.checked) {
  137. const input = document.createElement('input');
  138. input.type = 'hidden';
  139. input.name = el.name;
  140. input.value = el.value;
  141. evt.target.appendChild(input);
  142. }
  143. });
  144. evt.target.submit();
  145. },
  146. }));
  147. // Form state
  148. Alpine.store('form', {
  149. dirty: false,
  150. makeDirty() {
  151. this.dirty = true;
  152. },
  153. });
  154. document
  155. .querySelectorAll('#vstobjects input, #vstobjects select, #vstobjects textarea')
  156. .forEach((el) => {
  157. el.addEventListener('change', () => {
  158. Alpine.store('form').makeDirty();
  159. });
  160. });
  161. // Notifications data
  162. Alpine.data('notifications', () => ({
  163. initialized: false,
  164. open: false,
  165. notifications: [],
  166. toggle() {
  167. this.open = !this.open;
  168. if (!this.initialized) {
  169. this.list();
  170. }
  171. },
  172. async list() {
  173. const token = document.querySelector('#token').getAttribute('token');
  174. const res = await fetch(`/list/notifications/?ajax=1&token=${token}`);
  175. this.initialized = true;
  176. if (!res.ok) {
  177. throw new Error('An error occured while listing notifications.');
  178. }
  179. this.notifications = Object.entries(await res.json()).reduce(
  180. (acc, [_id, notification]) => [...acc, notification],
  181. []
  182. );
  183. },
  184. async remove(id) {
  185. const token = document.querySelector('#token').getAttribute('token');
  186. await fetch(`/delete/notification/?delete=1&notification_id=${id}&token=${token}`);
  187. this.notifications = this.notifications.filter((notification) => notification.ID != id);
  188. if (this.notifications.length == 0) {
  189. this.open = false;
  190. }
  191. },
  192. async removeAll() {
  193. const token = document.querySelector('#token').getAttribute('token');
  194. await fetch(`/delete/notification/?delete=1&token=${token}`);
  195. this.notifications = [];
  196. this.open = false;
  197. },
  198. }));
  199. });