EditUser.vue 7.0 KB

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