userAuth.ts 7.8 KB

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