EditUser.vue 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. <script setup lang="ts">
  2. import {
  3. defineAsyncComponent,
  4. ref,
  5. watch,
  6. onMounted,
  7. onBeforeUnmount
  8. } from "vue";
  9. import Toast from "toasters";
  10. import { storeToRefs } from "pinia";
  11. import ws from "@/ws";
  12. import validation from "@/validation";
  13. import { useEditUserStore } from "@/stores/editUser";
  14. import { useWebsocketsStore } from "@/stores/websockets";
  15. import { useModalsStore } from "@/stores/modals";
  16. import { useUserAuthStore } from "@/stores/userAuth";
  17. const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
  18. const QuickConfirm = defineAsyncComponent(
  19. () => import("@/components/QuickConfirm.vue")
  20. );
  21. const props = defineProps({
  22. modalUuid: { type: String, default: "" }
  23. });
  24. const editUserStore = useEditUserStore(props);
  25. const { socket } = useWebsocketsStore();
  26. const { userId, user } = storeToRefs(editUserStore);
  27. const { setUser } = editUserStore;
  28. const { closeCurrentModal } = useModalsStore();
  29. const { hasPermission } = useUserAuthStore();
  30. const ban = ref({ reason: "", expiresAt: "1h" });
  31. const init = () => {
  32. if (userId.value)
  33. socket.dispatch(`users.getUserFromId`, userId.value, res => {
  34. if (res.status === "success") {
  35. setUser(res.data);
  36. socket.dispatch("apis.joinRoom", `edit-user.${userId.value}`);
  37. socket.on(
  38. "event:user.removed",
  39. res => {
  40. if (res.data.userId === userId.value)
  41. closeCurrentModal();
  42. },
  43. { modalUuid: props.modalUuid }
  44. );
  45. } else {
  46. new Toast("User with that ID not found");
  47. closeCurrentModal();
  48. }
  49. });
  50. };
  51. const updateUsername = () => {
  52. const { username } = user.value;
  53. if (!validation.isLength(username, 2, 32))
  54. return new Toast("Username must have between 2 and 32 characters.");
  55. if (!validation.regex.custom("a-zA-Z0-9_-").test(username))
  56. return new Toast(
  57. "Invalid username format. Allowed characters: a-z, A-Z, 0-9, _ and -."
  58. );
  59. return socket.dispatch(
  60. `users.updateUsername`,
  61. user.value._id,
  62. username,
  63. res => {
  64. new Toast(res.message);
  65. }
  66. );
  67. };
  68. const updateEmail = () => {
  69. const email = user.value.email.address;
  70. if (!validation.isLength(email, 3, 254))
  71. return new Toast("Email must have between 3 and 254 characters.");
  72. if (
  73. email.indexOf("@") !== email.lastIndexOf("@") ||
  74. !validation.regex.emailSimple.test(email) ||
  75. !validation.regex.ascii.test(email)
  76. )
  77. return new Toast("Invalid email format.");
  78. return socket.dispatch(`users.updateEmail`, user.value._id, email, res => {
  79. new Toast(res.message);
  80. });
  81. };
  82. const updateRole = () => {
  83. socket.dispatch(
  84. `users.updateRole`,
  85. user.value._id,
  86. user.value.role,
  87. res => {
  88. new Toast(res.message);
  89. }
  90. );
  91. };
  92. const banUser = () => {
  93. const { reason } = ban.value;
  94. if (!validation.isLength(reason, 1, 64))
  95. return new Toast("Reason must have between 1 and 64 characters.");
  96. if (!validation.regex.ascii.test(reason))
  97. return new Toast(
  98. "Invalid reason format. Only ascii characters are allowed."
  99. );
  100. return socket.dispatch(
  101. `users.banUserById`,
  102. user.value._id,
  103. ban.value.reason,
  104. ban.value.expiresAt,
  105. res => {
  106. new Toast(res.message);
  107. }
  108. );
  109. };
  110. const resendVerificationEmail = () => {
  111. socket.dispatch(`users.resendVerifyEmail`, user.value._id, res => {
  112. new Toast(res.message);
  113. });
  114. };
  115. const requestPasswordReset = () => {
  116. socket.dispatch(`users.adminRequestPasswordReset`, user.value._id, res => {
  117. new Toast(res.message);
  118. });
  119. };
  120. const removeAccount = () => {
  121. socket.dispatch(`users.adminRemove`, user.value._id, res => {
  122. new Toast(res.message);
  123. });
  124. };
  125. const removeSessions = () => {
  126. socket.dispatch(`users.removeSessions`, user.value._id, res => {
  127. new Toast(res.message);
  128. });
  129. };
  130. // When the userId changes, run init. There can be a delay between the modal opening and the required data (userId) being available
  131. watch(userId, () => init());
  132. onMounted(() => {
  133. ws.onConnect(init);
  134. });
  135. onBeforeUnmount(() => {
  136. socket.dispatch("apis.leaveRoom", `edit-user.${userId.value}`, () => {});
  137. // Delete the Pinia store that was created for this modal, after all other cleanup tasks are performed
  138. editUserStore.$dispose();
  139. });
  140. </script>
  141. <template>
  142. <div>
  143. <modal title="Edit User">
  144. <template #body v-if="user && user._id">
  145. <div class="section">
  146. <label class="label"> Change username </label>
  147. <p class="control is-grouped">
  148. <span class="control is-expanded">
  149. <input
  150. v-model="user.username"
  151. class="input"
  152. type="text"
  153. placeholder="Username"
  154. autofocus
  155. />
  156. </span>
  157. <span
  158. v-if="hasPermission('users.update')"
  159. class="control"
  160. >
  161. <a class="button is-info" @click="updateUsername()"
  162. >Update Username</a
  163. >
  164. </span>
  165. </p>
  166. <label class="label"> Change email address </label>
  167. <p class="control is-grouped">
  168. <span class="control is-expanded">
  169. <input
  170. v-model="user.email.address"
  171. class="input"
  172. type="text"
  173. placeholder="Email Address"
  174. autofocus
  175. />
  176. </span>
  177. <span
  178. v-if="hasPermission('users.update')"
  179. class="control"
  180. >
  181. <a class="button is-info" @click="updateEmail()"
  182. >Update Email Address</a
  183. >
  184. </span>
  185. </p>
  186. <label class="label"> Change user role </label>
  187. <div class="control is-grouped">
  188. <div class="control is-expanded select">
  189. <select
  190. v-model="user.role"
  191. :disabled="
  192. !hasPermission('users.update.restricted')
  193. "
  194. >
  195. <option>user</option>
  196. <option>moderator</option>
  197. <option>admin</option>
  198. </select>
  199. </div>
  200. <p
  201. v-if="hasPermission('users.update.restricted')"
  202. class="control"
  203. >
  204. <a class="button is-info" @click="updateRole()"
  205. >Update Role</a
  206. >
  207. </p>
  208. </div>
  209. </div>
  210. <div v-if="hasPermission('users.ban')" class="section">
  211. <label class="label"> Punish/Ban User </label>
  212. <p class="control is-grouped">
  213. <span class="control select">
  214. <select v-model="ban.expiresAt">
  215. <option value="1h">1 Hour</option>
  216. <option value="12h">12 Hours</option>
  217. <option value="1d">1 Day</option>
  218. <option value="1w">1 Week</option>
  219. <option value="1m">1 Month</option>
  220. <option value="3m">3 Months</option>
  221. <option value="6m">6 Months</option>
  222. <option value="1y">1 Year</option>
  223. </select>
  224. </span>
  225. <span class="control is-expanded">
  226. <input
  227. v-model="ban.reason"
  228. class="input"
  229. type="text"
  230. placeholder="Ban reason"
  231. autofocus
  232. />
  233. </span>
  234. <span class="control">
  235. <a class="button is-danger" @click="banUser()">
  236. Ban user
  237. </a>
  238. </span>
  239. </p>
  240. </div>
  241. </template>
  242. <template #footer>
  243. <quick-confirm
  244. v-if="hasPermission('users.resendVerifyEmail')"
  245. @confirm="resendVerificationEmail()"
  246. >
  247. <a class="button is-warning"> Resend verification email </a>
  248. </quick-confirm>
  249. <quick-confirm
  250. v-if="hasPermission('users.requestPasswordReset')"
  251. @confirm="requestPasswordReset()"
  252. >
  253. <a class="button is-warning"> Request password reset </a>
  254. </quick-confirm>
  255. <quick-confirm
  256. v-if="hasPermission('users.remove.sessions')"
  257. @confirm="removeSessions()"
  258. >
  259. <a class="button is-warning"> Remove all sessions </a>
  260. </quick-confirm>
  261. <quick-confirm
  262. v-if="hasPermission('users.remove')"
  263. @confirm="removeAccount()"
  264. >
  265. <a class="button is-danger"> Remove account </a>
  266. </quick-confirm>
  267. </template>
  268. </modal>
  269. </div>
  270. </template>
  271. <style lang="less" scoped>
  272. .night-mode .section {
  273. background-color: transparent !important;
  274. }
  275. .section {
  276. padding: 15px 0 !important;
  277. }
  278. .save-changes {
  279. color: var(--white);
  280. }
  281. .tag:not(:last-child) {
  282. margin-right: 5px;
  283. }
  284. .select:after {
  285. border-color: var(--primary-color);
  286. }
  287. </style>