userAuth.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import { defineStore } from "pinia";
  2. import { computed, ref } from "vue";
  3. import validation from "@/validation";
  4. import { useWebsocketStore } from "@/stores/websocket";
  5. import { useConfigStore } from "@/stores/config";
  6. import { User } from "@/types/user";
  7. export const useUserAuthStore = defineStore("userAuth", () => {
  8. const configStore = useConfigStore();
  9. const websocketStore = useWebsocketStore();
  10. const userIdMap = ref<Record<string, { name: string; username: string }>>(
  11. {}
  12. );
  13. const userIdRequested = ref<Record<string, boolean>>({});
  14. const pendingUserIdCallbacks = ref<
  15. Record<
  16. string,
  17. ((basicUser: { name: string; username: string }) => void)[]
  18. >
  19. >({});
  20. const currentUser = ref<User | null>();
  21. const banned = ref(false);
  22. const ban = ref<{
  23. reason?: string;
  24. expiresAt?: number;
  25. } | null>({
  26. reason: null,
  27. expiresAt: null
  28. });
  29. const gotData = ref(false);
  30. const gotPermissions = ref(false);
  31. const permissions = ref<Record<string, boolean>>({});
  32. const loggedIn = computed(() => !!currentUser.value);
  33. const register = async (user: {
  34. username: string;
  35. email: string;
  36. password: string;
  37. recaptchaToken: string;
  38. }) => {
  39. const { username, email, password, recaptchaToken } = user;
  40. if (!email || !username || !password)
  41. throw new Error("Please fill in all fields");
  42. if (!validation.isLength(email, 3, 254))
  43. throw new Error("Email must have between 3 and 254 characters.");
  44. if (
  45. email.indexOf("@") !== email.lastIndexOf("@") ||
  46. !validation.regex.emailSimple.test(email)
  47. )
  48. throw new Error("Invalid email format.");
  49. if (!validation.isLength(username, 2, 32))
  50. throw new Error("Username must have between 2 and 32 characters.");
  51. if (!validation.regex.azAZ09_.test(username))
  52. throw new Error(
  53. "Invalid username format. Allowed characters: a-z, A-Z, 0-9 and _."
  54. );
  55. if (username.replaceAll(/[_]/g, "").length === 0)
  56. throw new Error(
  57. "Invalid username format. Allowed characters: a-z, A-Z, 0-9 and _, and there has to be at least one letter or number."
  58. );
  59. if (!validation.isLength(password, 6, 200))
  60. throw new Error("Password must have between 6 and 200 characters.");
  61. if (!validation.regex.password.test(password))
  62. throw new Error(
  63. "Invalid password format. Must have one lowercase letter, one uppercase letter, one number and one special character."
  64. );
  65. const data = await websocketStore.runJob("users.register", {
  66. username,
  67. email,
  68. password,
  69. recaptchaToken
  70. });
  71. if (!data?.SID) throw new Error("You must login");
  72. const date = new Date();
  73. date.setTime(new Date().getTime() + 2 * 365 * 24 * 60 * 60 * 1000);
  74. const secure = configStore.urls.secure ? "secure=true; " : "";
  75. let domain = "";
  76. if (configStore.urls.host !== "localhost")
  77. domain = ` domain=${configStore.urls.host};`;
  78. document.cookie = `${configStore.cookie}=${
  79. data.SID
  80. }; expires=${date.toUTCString()}; ${domain}${secure}path=/`;
  81. };
  82. const login = async (user: { email: string; password: string }) => {
  83. const { email, password } = user;
  84. const data = await websocketStore.runJob("users.login", {
  85. email,
  86. password
  87. });
  88. const date = new Date();
  89. date.setTime(new Date().getTime() + 2 * 365 * 24 * 60 * 60 * 1000);
  90. const secure = configStore.urls.secure ? "secure=true; " : "";
  91. let domain = "";
  92. if (configStore.urls.host !== "localhost")
  93. domain = ` domain=${configStore.urls.host};`;
  94. document.cookie = `${configStore.cookie}=${
  95. data.SID
  96. }; expires=${date.toUTCString()}; ${domain}${secure}path=/`;
  97. const bc = new BroadcastChannel(`${configStore.cookie}.user_login`);
  98. bc.postMessage(true);
  99. bc.close();
  100. };
  101. const logout = async () => {
  102. await websocketStore.runJob("users.logout", {});
  103. document.cookie = `${configStore.cookie}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
  104. window.location.reload();
  105. };
  106. const mapUserId = (data: {
  107. userId: string;
  108. user: { name: string; username: string };
  109. }) => {
  110. userIdMap.value[`Z${data.userId}`] = data.user;
  111. userIdRequested.value[`Z${data.userId}`] = false;
  112. };
  113. const requestingUserId = (userId: string) => {
  114. userIdRequested.value[`Z${userId}`] = true;
  115. if (!pendingUserIdCallbacks.value[`Z${userId}`])
  116. pendingUserIdCallbacks.value[`Z${userId}`] = [];
  117. };
  118. const pendingUser = (
  119. userId: string,
  120. callback: (basicUser: { name: string; username: string }) => void
  121. ) => {
  122. pendingUserIdCallbacks.value[`Z${userId}`].push(callback);
  123. };
  124. const clearPendingCallbacks = (userId: string) => {
  125. pendingUserIdCallbacks.value[`Z${userId}`] = [];
  126. };
  127. const getBasicUser = async (userId: string) =>
  128. new Promise((resolve, reject) => {
  129. if (typeof userIdMap.value[`Z${userId}`] === "string") {
  130. resolve(userIdMap.value[`Z${userId}`]);
  131. return;
  132. }
  133. if (userIdRequested.value[`Z${userId}`] === true) {
  134. pendingUser(userId, user => resolve(user));
  135. return;
  136. }
  137. requestingUserId(userId);
  138. // TODO use model store for this?
  139. websocketStore
  140. .runJob("data.users.findById", { _id: userId })
  141. .then(user => {
  142. mapUserId({
  143. userId,
  144. user
  145. });
  146. pendingUserIdCallbacks.value[`Z${userId}`].forEach(cb =>
  147. cb(user)
  148. );
  149. clearPendingCallbacks(userId);
  150. resolve(user);
  151. })
  152. .catch(reject);
  153. });
  154. const banUser = (data: { reason: string; expiresAt: number }) => {
  155. banned.value = true;
  156. ban.value = data;
  157. };
  158. const hasPermission = (permission: string) =>
  159. !!(permissions.value && permissions.value[permission]);
  160. const updatePermissions = () =>
  161. websocketStore.runJob("data.users.getPermissions", {}).then(data => {
  162. permissions.value = data;
  163. gotPermissions.value = true;
  164. });
  165. const resetCookieExpiration = () => {
  166. const cookies = {};
  167. document.cookie.split("; ").forEach(cookie => {
  168. cookies[cookie.substring(0, cookie.indexOf("="))] =
  169. cookie.substring(cookie.indexOf("=") + 1, cookie.length);
  170. });
  171. const SIDName = configStore.cookie;
  172. if (!cookies[SIDName]) return;
  173. const date = new Date();
  174. date.setTime(new Date().getTime() + 2 * 365 * 24 * 60 * 60 * 1000);
  175. const secure = configStore.urls.secure ? "secure=true; " : "";
  176. let domain = "";
  177. if (configStore.urls.host !== "localhost")
  178. domain = ` domain=${configStore.urls.host};`;
  179. document.cookie = `${configStore.cookie}=${
  180. cookies[SIDName]
  181. }; expires=${date.toUTCString()}; ${domain}${secure}path=/`;
  182. };
  183. return {
  184. userIdMap,
  185. userIdRequested,
  186. pendingUserIdCallbacks,
  187. currentUser,
  188. banned,
  189. ban,
  190. gotData,
  191. gotPermissions,
  192. permissions,
  193. loggedIn,
  194. register,
  195. login,
  196. logout,
  197. mapUserId,
  198. requestingUserId,
  199. pendingUser,
  200. clearPendingCallbacks,
  201. getBasicUser,
  202. banUser,
  203. hasPermission,
  204. updatePermissions,
  205. resetCookieExpiration
  206. };
  207. });