EditUser.vue 7.3 KB

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