userAuth.ts 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. import { defineStore } from "pinia";
  2. import Toast from "toasters";
  3. import validation from "@/validation";
  4. import { useWebsocketsStore } from "@/stores/websockets";
  5. import { useConfigStore } from "@/stores/config";
  6. export const useUserAuthStore = defineStore("userAuth", {
  7. state: (): {
  8. userIdMap: Record<string, { name: string; username: string }>;
  9. userIdRequested: Record<string, boolean>;
  10. pendingUserIdCallbacks: Record<
  11. string,
  12. ((basicUser: { name: string; username: string }) => void)[]
  13. >;
  14. loggedIn: boolean;
  15. role: "user" | "moderator" | "admin";
  16. username: string;
  17. email: string;
  18. userId: string;
  19. banned: boolean;
  20. ban: {
  21. reason: string;
  22. expiresAt: number;
  23. };
  24. gotData: boolean;
  25. gotPermissions: boolean;
  26. permissions: Record<string, boolean>;
  27. } => ({
  28. userIdMap: {},
  29. userIdRequested: {},
  30. pendingUserIdCallbacks: {},
  31. loggedIn: false,
  32. role: "",
  33. username: "",
  34. email: "",
  35. userId: "",
  36. banned: false,
  37. ban: {
  38. reason: null,
  39. expiresAt: null
  40. },
  41. gotData: false,
  42. gotPermissions: false,
  43. permissions: {}
  44. }),
  45. actions: {
  46. register(user: {
  47. username: string;
  48. email: string;
  49. password: string;
  50. recaptchaToken: string;
  51. }) {
  52. return new Promise((resolve, reject) => {
  53. const { username, email, password, recaptchaToken } = user;
  54. if (!email || !username || !password)
  55. reject(new Error("Please fill in all fields"));
  56. else if (!validation.isLength(email, 3, 254))
  57. reject(
  58. new Error(
  59. "Email must have between 3 and 254 characters."
  60. )
  61. );
  62. else if (
  63. email.indexOf("@") !== email.lastIndexOf("@") ||
  64. !validation.regex.emailSimple.test(email)
  65. )
  66. reject(new Error("Invalid email format."));
  67. else if (!validation.isLength(username, 2, 32))
  68. reject(
  69. new Error(
  70. "Username must have between 2 and 32 characters."
  71. )
  72. );
  73. else if (!validation.regex.azAZ09_.test(username))
  74. reject(
  75. new Error(
  76. "Invalid username format. Allowed characters: a-z, A-Z, 0-9 and _."
  77. )
  78. );
  79. else if (username.replaceAll(/[_]/g, "").length === 0)
  80. reject(
  81. new Error(
  82. "Invalid username format. Allowed characters: a-z, A-Z, 0-9 and _, and there has to be at least one letter or number."
  83. )
  84. );
  85. else if (!validation.isLength(password, 6, 200))
  86. reject(
  87. new Error(
  88. "Password must have between 6 and 200 characters."
  89. )
  90. );
  91. else if (!validation.regex.password.test(password))
  92. reject(
  93. new Error(
  94. "Invalid password format. Must have one lowercase letter, one uppercase letter, one number and one special character."
  95. )
  96. );
  97. else {
  98. const { socket } = useWebsocketsStore();
  99. const configStore = useConfigStore();
  100. socket.dispatch(
  101. "users.register",
  102. username,
  103. email,
  104. password,
  105. recaptchaToken,
  106. res => {
  107. if (res.status === "success") {
  108. if (res.SID) {
  109. const date = new Date();
  110. date.setTime(
  111. new Date().getTime() +
  112. 2 * 365 * 24 * 60 * 60 * 1000
  113. );
  114. const secure = configStore.urls.secure
  115. ? "secure=true; "
  116. : "";
  117. let domain = "";
  118. if (configStore.urls.host !== "localhost")
  119. domain = ` domain=${configStore.urls.host};`;
  120. document.cookie = `${configStore.cookie}=${
  121. res.SID
  122. }; expires=${date.toUTCString()}; ${domain}${secure}path=/`;
  123. return resolve({
  124. status: "success",
  125. message: "Account registered!"
  126. });
  127. }
  128. return reject(new Error("You must login"));
  129. }
  130. return reject(new Error(res.message));
  131. }
  132. );
  133. }
  134. });
  135. },
  136. login(user: { email: string; password: string }) {
  137. return new Promise((resolve, reject) => {
  138. const { email, password } = user;
  139. const { socket } = useWebsocketsStore();
  140. const configStore = useConfigStore();
  141. socket.dispatch("users.login", email, password, res => {
  142. if (res.status === "success") {
  143. const date = new Date();
  144. date.setTime(
  145. new Date().getTime() + 2 * 365 * 24 * 60 * 60 * 1000
  146. );
  147. const secure = configStore.urls.secure
  148. ? "secure=true; "
  149. : "";
  150. let domain = "";
  151. if (configStore.urls.host !== "localhost")
  152. domain = ` domain=${configStore.urls.host};`;
  153. document.cookie = `${configStore.cookie}=${
  154. res.data.SID
  155. }; expires=${date.toUTCString()}; ${domain}${secure}path=/`;
  156. const bc = new BroadcastChannel(
  157. `${configStore.cookie}.user_login`
  158. );
  159. bc.postMessage(true);
  160. bc.close();
  161. return resolve({
  162. status: "success",
  163. message: "Logged in!"
  164. });
  165. }
  166. return reject(new Error(res.message));
  167. });
  168. });
  169. },
  170. logout() {
  171. return new Promise((resolve, reject) => {
  172. const { socket } = useWebsocketsStore();
  173. socket.dispatch("users.logout", res => {
  174. if (res.status === "success") {
  175. const configStore = useConfigStore();
  176. document.cookie = `${configStore.cookie}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
  177. window.location.reload();
  178. return resolve(true);
  179. }
  180. new Toast(res.message);
  181. return reject(new Error(res.message));
  182. });
  183. });
  184. },
  185. getBasicUser(userId: string) {
  186. return new Promise(
  187. (
  188. resolve: (
  189. basicUser: { name: string; username: string } | null
  190. ) => void
  191. ) => {
  192. if (typeof this.userIdMap[`Z${userId}`] !== "string") {
  193. if (this.userIdRequested[`Z${userId}`] !== true) {
  194. this.requestingUserId(userId);
  195. const { socket } = useWebsocketsStore();
  196. socket.dispatch(
  197. "users.getBasicUser",
  198. userId,
  199. res => {
  200. if (res.status === "success") {
  201. const user = res.data;
  202. this.mapUserId({
  203. userId,
  204. user: {
  205. name: user.name,
  206. username: user.username
  207. }
  208. });
  209. this.pendingUserIdCallbacks[
  210. `Z${userId}`
  211. ].forEach(cb => cb(user));
  212. this.clearPendingCallbacks(userId);
  213. return resolve(user);
  214. }
  215. return resolve(null);
  216. }
  217. );
  218. } else {
  219. this.pendingUser(userId, user => resolve(user));
  220. }
  221. } else {
  222. resolve(this.userIdMap[`Z${userId}`]);
  223. }
  224. }
  225. );
  226. },
  227. mapUserId(data: {
  228. userId: string;
  229. user: { name: string; username: string };
  230. }) {
  231. this.userIdMap[`Z${data.userId}`] = data.user;
  232. this.userIdRequested[`Z${data.userId}`] = false;
  233. },
  234. requestingUserId(userId: string) {
  235. this.userIdRequested[`Z${userId}`] = true;
  236. if (!this.pendingUserIdCallbacks[`Z${userId}`])
  237. this.pendingUserIdCallbacks[`Z${userId}`] = [];
  238. },
  239. pendingUser(
  240. userId: string,
  241. callback: (basicUser: { name: string; username: string }) => void
  242. ) {
  243. this.pendingUserIdCallbacks[`Z${userId}`].push(callback);
  244. },
  245. clearPendingCallbacks(userId: string) {
  246. this.pendingUserIdCallbacks[`Z${userId}`] = [];
  247. },
  248. authData(data: {
  249. loggedIn: boolean;
  250. role: string;
  251. username: string;
  252. email: string;
  253. userId: string;
  254. }) {
  255. this.loggedIn = data.loggedIn;
  256. this.role = data.role;
  257. this.username = data.username;
  258. this.email = data.email;
  259. this.userId = data.userId;
  260. this.gotData = true;
  261. },
  262. banUser(ban: { reason: string; expiresAt: number }) {
  263. this.banned = true;
  264. this.ban = ban;
  265. },
  266. updateUsername(username: string) {
  267. this.username = username;
  268. },
  269. updateRole(role: string) {
  270. this.role = role;
  271. },
  272. hasPermission(permission: string) {
  273. return !!(this.permissions && this.permissions[permission]);
  274. },
  275. updatePermissions() {
  276. return new Promise(resolve => {
  277. const { socket } = useWebsocketsStore();
  278. socket.dispatch("utils.getPermissions", res => {
  279. this.permissions = res.data.permissions;
  280. this.gotPermissions = true;
  281. resolve(this.permissions);
  282. });
  283. });
  284. },
  285. resetCookieExpiration() {
  286. const cookies = {};
  287. document.cookie.split("; ").forEach(cookie => {
  288. cookies[cookie.substring(0, cookie.indexOf("="))] =
  289. cookie.substring(cookie.indexOf("=") + 1, cookie.length);
  290. });
  291. const configStore = useConfigStore();
  292. const SIDName = configStore.cookie;
  293. if (!cookies[SIDName]) return;
  294. const date = new Date();
  295. date.setTime(new Date().getTime() + 2 * 365 * 24 * 60 * 60 * 1000);
  296. const secure = configStore.urls.secure ? "secure=true; " : "";
  297. let domain = "";
  298. if (configStore.urls.host !== "localhost")
  299. domain = ` domain=${configStore.urls.host};`;
  300. document.cookie = `${configStore.cookie}=${
  301. cookies[SIDName]
  302. }; expires=${date.toUTCString()}; ${domain}${secure}path=/`;
  303. }
  304. }
  305. });