EditUser.vue 7.1 KB

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