EditUser.vue 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  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. const mapModalState = function(namespace, map) {
  110. const modalState = {};
  111. Object.entries(map).forEach(([mapKey, mapValue]) => {
  112. modalState[mapKey] = function() {
  113. return mapValue(namespace.replace("MODAL_UUID", this.modalUuid).split("/").reduce((a, b) => a[b], this.$store.state));
  114. }
  115. });
  116. return modalState;
  117. }
  118. const mapModalActions = function(namespace, map) {
  119. const modalState = {};
  120. map.forEach(mapValue => {
  121. modalState[mapValue] = function(value) {
  122. return this.$store.dispatch(`${namespace.replace("MODAL_UUID", this.modalUuid)}/${mapValue}`, value);
  123. }
  124. });
  125. return modalState;
  126. }
  127. export default {
  128. props: {
  129. modalUuid: { type: String, default: "" }
  130. },
  131. data() {
  132. return {
  133. ban: {
  134. expiresAt: "1h"
  135. }
  136. };
  137. },
  138. computed: {
  139. ...mapModalState("modals/editUser/MODAL_UUID", {
  140. userId: state => state.userId,
  141. user: state => state.user
  142. }),
  143. ...mapGetters({
  144. socket: "websockets/getSocket"
  145. })
  146. },
  147. watch: {
  148. // When the userId changes, run init. There can be a delay between the modal opening and the required data (userId) being available
  149. userId() {
  150. // Note: is it possible for this to run before the socket is ready?
  151. this.init();
  152. }
  153. },
  154. mounted() {
  155. ws.onConnect(this.init);
  156. },
  157. beforeUnmount() {
  158. this.socket.dispatch(
  159. "apis.leaveRoom",
  160. `edit-user.${this.userId}`,
  161. () => {}
  162. );
  163. // Delete the VueX module that was created for this modal, after all other cleanup tasks are performed
  164. this.$store.unregisterModule(["modals", "editUser", this.modalUuid]);
  165. },
  166. methods: {
  167. init() {
  168. if (this.userId)
  169. this.socket.dispatch(
  170. `users.getUserFromId`,
  171. this.userId,
  172. res => {
  173. if (res.status === "success") {
  174. const user = res.data;
  175. this.setUser(user);
  176. this.socket.dispatch(
  177. "apis.joinRoom",
  178. `edit-user.${this.userId}`
  179. );
  180. this.socket.on(
  181. "event:user.removed",
  182. res => {
  183. if (res.data.userId === this.userId)
  184. this.closeModal("editUser");
  185. },
  186. { modal: "editUser" }
  187. );
  188. } else {
  189. new Toast("User with that ID not found");
  190. this.closeModal("editUser");
  191. }
  192. }
  193. );
  194. },
  195. updateUsername() {
  196. const { username } = this.user;
  197. if (!validation.isLength(username, 2, 32))
  198. return new Toast(
  199. "Username must have between 2 and 32 characters."
  200. );
  201. if (!validation.regex.custom("a-zA-Z0-9_-").test(username))
  202. return new Toast(
  203. "Invalid username format. Allowed characters: a-z, A-Z, 0-9, _ and -."
  204. );
  205. return this.socket.dispatch(
  206. `users.updateUsername`,
  207. this.user._id,
  208. username,
  209. res => {
  210. new Toast(res.message);
  211. }
  212. );
  213. },
  214. updateEmail() {
  215. const email = this.user.email.address;
  216. if (!validation.isLength(email, 3, 254))
  217. return new Toast(
  218. "Email must have between 3 and 254 characters."
  219. );
  220. if (
  221. email.indexOf("@") !== email.lastIndexOf("@") ||
  222. !validation.regex.emailSimple.test(email) ||
  223. !validation.regex.ascii.test(email)
  224. )
  225. return new Toast("Invalid email format.");
  226. return this.socket.dispatch(
  227. `users.updateEmail`,
  228. this.user._id,
  229. email,
  230. res => {
  231. new Toast(res.message);
  232. }
  233. );
  234. },
  235. updateRole() {
  236. this.socket.dispatch(
  237. `users.updateRole`,
  238. this.user._id,
  239. this.user.role,
  240. res => {
  241. new Toast(res.message);
  242. }
  243. );
  244. },
  245. banUser() {
  246. const { reason } = this.ban;
  247. if (!validation.isLength(reason, 1, 64))
  248. return new Toast(
  249. "Reason must have between 1 and 64 characters."
  250. );
  251. if (!validation.regex.ascii.test(reason))
  252. return new Toast(
  253. "Invalid reason format. Only ascii characters are allowed."
  254. );
  255. return this.socket.dispatch(
  256. `users.banUserById`,
  257. this.user._id,
  258. this.ban.reason,
  259. this.ban.expiresAt,
  260. res => {
  261. new Toast(res.message);
  262. }
  263. );
  264. },
  265. resendVerificationEmail() {
  266. this.socket.dispatch(
  267. `users.resendVerifyEmail`,
  268. this.user._id,
  269. res => {
  270. new Toast(res.message);
  271. }
  272. );
  273. },
  274. requestPasswordReset() {
  275. this.socket.dispatch(
  276. `users.adminRequestPasswordReset`,
  277. this.user._id,
  278. res => {
  279. new Toast(res.message);
  280. }
  281. );
  282. },
  283. removeAccount() {
  284. this.socket.dispatch(`users.adminRemove`, this.user._id, res => {
  285. new Toast(res.message);
  286. });
  287. },
  288. removeSessions() {
  289. this.socket.dispatch(`users.removeSessions`, this.user._id, res => {
  290. new Toast(res.message);
  291. });
  292. },
  293. ...mapModalActions("modals/editUser/MODAL_UUID", ["setUser"]),
  294. ...mapActions("modalVisibility", ["closeModal"])
  295. }
  296. };
  297. </script>
  298. <style lang="less" scoped>
  299. .night-mode .section {
  300. background-color: transparent !important;
  301. }
  302. .section {
  303. padding: 15px 0 !important;
  304. }
  305. .save-changes {
  306. color: var(--white);
  307. }
  308. .tag:not(:last-child) {
  309. margin-right: 5px;
  310. }
  311. .select:after {
  312. border-color: var(--primary-color);
  313. }
  314. </style>