main.js 5.7 KB

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