Settings.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  1. <template>
  2. <div>
  3. <metadata title="Settings" />
  4. <main-header />
  5. <div class="container">
  6. <div class="buttons">
  7. <button
  8. :class="{ active: activeTab === 'profile' }"
  9. @click="switchTab('profile')"
  10. >
  11. Profile
  12. </button>
  13. <button
  14. :class="{ active: activeTab === 'account' }"
  15. @click="switchTab('account')"
  16. >
  17. Account
  18. </button>
  19. <button
  20. :class="{ active: activeTab === 'security' }"
  21. @click="switchTab('security')"
  22. >
  23. Security
  24. </button>
  25. <button
  26. :class="{ active: activeTab === 'preferences' }"
  27. @click="switchTab('preferences')"
  28. >
  29. Preferences
  30. </button>
  31. </div>
  32. <div class="content profile-tab" v-if="activeTab === 'profile'">
  33. <p class="control is-expanded">
  34. <label for="name">Name</label>
  35. <input
  36. class="input"
  37. id="name"
  38. type="text"
  39. placeholder="Name"
  40. v-model="user.name"
  41. />
  42. </p>
  43. <p class="control is-expanded">
  44. <label for="location">Location</label>
  45. <input
  46. class="input"
  47. id="location"
  48. type="text"
  49. placeholder="Location"
  50. v-model="user.location"
  51. />
  52. </p>
  53. <p class="control is-expanded">
  54. <label for="bio">Bio</label>
  55. <textarea
  56. class="textarea"
  57. id="bio"
  58. placeholder="Bio"
  59. v-model="user.bio"
  60. />
  61. </p>
  62. <button class="button is-primary" @click="saveChangesProfile()">
  63. Save changes
  64. </button>
  65. </div>
  66. <div class="content account-tab" v-if="activeTab === 'account'">
  67. <p class="control is-expanded">
  68. <label for="name">Username</label>
  69. <input
  70. class="input"
  71. id="username"
  72. type="text"
  73. placeholder="Username"
  74. v-model="user.username"
  75. />
  76. </p>
  77. <p class="control is-expanded">
  78. <label for="location">Email</label>
  79. <input
  80. class="input"
  81. id="email"
  82. type="text"
  83. placeholder="Email"
  84. v-model="user.email.address"
  85. />
  86. </p>
  87. <button class="button is-primary" @click="saveChangesAccount()">
  88. Save changes
  89. </button>
  90. </div>
  91. <div class="content security-tab" v-if="activeTab === 'security'">
  92. <label v-if="!password" class="label">Add password</label>
  93. <div v-if="!password" class="control is-grouped">
  94. <button
  95. v-if="passwordStep === 1"
  96. class="button is-success"
  97. @click="requestPassword()"
  98. >
  99. Request password email
  100. </button>
  101. <br />
  102. <p
  103. v-if="passwordStep === 2"
  104. class="control is-expanded has-icon has-icon-right"
  105. >
  106. <input
  107. v-model="passwordCode"
  108. class="input"
  109. type="text"
  110. placeholder="Code"
  111. />
  112. </p>
  113. <p v-if="passwordStep === 2" class="control is-expanded">
  114. <button
  115. class="button is-success"
  116. v-on:click="verifyCode()"
  117. >
  118. Verify code
  119. </button>
  120. </p>
  121. <p
  122. v-if="passwordStep === 3"
  123. class="control is-expanded has-icon has-icon-right"
  124. >
  125. <input
  126. v-model="setNewPassword"
  127. class="input"
  128. type="password"
  129. placeholder="New password"
  130. />
  131. </p>
  132. <p v-if="passwordStep === 3" class="control is-expanded">
  133. <button
  134. class="button is-success"
  135. @click="setPassword()"
  136. >
  137. Set password
  138. </button>
  139. </p>
  140. </div>
  141. <a
  142. v-if="passwordStep === 1 && !password"
  143. href="#"
  144. @click="passwordStep = 2"
  145. >Skip this step</a
  146. >
  147. <a
  148. v-if="!github"
  149. class="button is-github"
  150. :href="`${serverDomain}/auth/github/link`"
  151. >
  152. <div class="icon">
  153. <img class="invert" src="/assets/social/github.svg" />
  154. </div>
  155. &nbsp; Link GitHub to account
  156. </a>
  157. <button
  158. v-if="password && github"
  159. class="button is-danger"
  160. @click="unlinkPassword()"
  161. >
  162. Remove logging in with password
  163. </button>
  164. <button
  165. v-if="password && github"
  166. class="button is-danger"
  167. @click="unlinkGitHub()"
  168. >
  169. Remove logging in with GitHub
  170. </button>
  171. <br />
  172. <button
  173. class="button is-warning"
  174. style="margin-top: 30px;"
  175. @click="removeSessions()"
  176. >
  177. Log out everywhere
  178. </button>
  179. </div>
  180. <div
  181. class="content preferences-tab"
  182. v-if="activeTab === 'preferences'"
  183. >
  184. <p class="control is-expanded checkbox-control">
  185. <input
  186. type="checkbox"
  187. id="nightmode"
  188. v-model="localNightmode"
  189. />
  190. <label for="nightmode">
  191. <p>Use nightmode</p>
  192. <span></span>
  193. </label>
  194. </p>
  195. <button
  196. class="button is-primary"
  197. @click="saveChangesPreferences()"
  198. >
  199. Save changes
  200. </button>
  201. </div>
  202. </div>
  203. <main-footer />
  204. </div>
  205. </template>
  206. <script>
  207. import { mapState, mapActions } from "vuex";
  208. import Toast from "toasters";
  209. import MainHeader from "../MainHeader.vue";
  210. import MainFooter from "../MainFooter.vue";
  211. import io from "../../io";
  212. import validation from "../../validation";
  213. export default {
  214. components: { MainHeader, MainFooter },
  215. data() {
  216. return {
  217. user: {},
  218. newPassword: "",
  219. password: false,
  220. github: false,
  221. setNewPassword: "",
  222. passwordStep: 1,
  223. passwordCode: "",
  224. serverDomain: "",
  225. activeTab: "profile",
  226. localNightmode: false
  227. };
  228. },
  229. computed: mapState({
  230. userId: state => state.user.auth.userId,
  231. nightmode: state => state.user.preferences.nightmode
  232. }),
  233. mounted() {
  234. this.localNightmode = this.nightmode;
  235. lofig.get("serverDomain").then(serverDomain => {
  236. this.serverDomain = serverDomain;
  237. });
  238. io.getSocket(socket => {
  239. this.socket = socket;
  240. this.socket.emit("users.findBySession", res => {
  241. if (res.status === "success") {
  242. this.user = res.data;
  243. this.password = this.user.password;
  244. this.github = this.user.github;
  245. } else {
  246. new Toast({
  247. content: "Your are currently not signed in",
  248. timeout: 3000
  249. });
  250. }
  251. });
  252. this.socket.on("event:user.linkPassword", () => {
  253. this.password = true;
  254. });
  255. this.socket.on("event:user.linkGitHub", () => {
  256. this.github = true;
  257. });
  258. this.socket.on("event:user.unlinkPassword", () => {
  259. this.password = false;
  260. });
  261. this.socket.on("event:user.unlinkGitHub", () => {
  262. this.github = false;
  263. });
  264. });
  265. },
  266. methods: {
  267. switchTab(tabName) {
  268. this.activeTab = tabName;
  269. },
  270. saveChangesProfile() {
  271. this.changeName();
  272. this.changeLocation();
  273. this.changeBio();
  274. },
  275. saveChangesAccount() {
  276. this.changeUsername();
  277. this.changeEmail();
  278. },
  279. saveChangesPreferences() {
  280. this.changeNightmodeLocal();
  281. },
  282. changeEmail() {
  283. const email = this.user.email.address;
  284. if (!validation.isLength(email, 3, 254))
  285. return new Toast({
  286. content: "Email must have between 3 and 254 characters.",
  287. timeout: 8000
  288. });
  289. if (
  290. email.indexOf("@") !== email.lastIndexOf("@") ||
  291. !validation.regex.emailSimple.test(email)
  292. )
  293. return new Toast({
  294. content: "Invalid email format.",
  295. timeout: 8000
  296. });
  297. return this.socket.emit(
  298. "users.updateEmail",
  299. this.userId,
  300. email,
  301. res => {
  302. if (res.status !== "success")
  303. new Toast({ content: res.message, timeout: 8000 });
  304. else
  305. new Toast({
  306. content: "Successfully changed email address",
  307. timeout: 4000
  308. });
  309. }
  310. );
  311. },
  312. changeUsername() {
  313. const { username } = this.user;
  314. if (!validation.isLength(username, 2, 32))
  315. return new Toast({
  316. content: "Username must have between 2 and 32 characters.",
  317. timeout: 8000
  318. });
  319. if (!validation.regex.azAZ09_.test(username))
  320. return new Toast({
  321. content:
  322. "Invalid username format. Allowed characters: a-z, A-Z, 0-9 and _.",
  323. timeout: 8000
  324. });
  325. return this.socket.emit(
  326. "users.updateUsername",
  327. this.userId,
  328. username,
  329. res => {
  330. if (res.status !== "success")
  331. new Toast({ content: res.message, timeout: 8000 });
  332. else
  333. new Toast({
  334. content: "Successfully changed username",
  335. timeout: 4000
  336. });
  337. }
  338. );
  339. },
  340. changeName() {
  341. const { name } = this.user;
  342. if (!validation.isLength(name, 1, 64))
  343. return new Toast({
  344. content: "Name must have between 1 and 64 characters.",
  345. timeout: 8000
  346. });
  347. return this.socket.emit(
  348. "users.updateName",
  349. this.userId,
  350. name,
  351. res => {
  352. if (res.status !== "success")
  353. new Toast({ content: res.message, timeout: 8000 });
  354. else
  355. new Toast({
  356. content: "Successfully changed name",
  357. timeout: 4000
  358. });
  359. }
  360. );
  361. },
  362. changeLocation() {
  363. const { location } = this.user;
  364. if (!validation.isLength(location, 0, 50))
  365. return new Toast({
  366. content: "Location must have between 0 and 50 characters.",
  367. timeout: 8000
  368. });
  369. return this.socket.emit(
  370. "users.updateLocation",
  371. this.userId,
  372. location,
  373. res => {
  374. if (res.status !== "success")
  375. new Toast({ content: res.message, timeout: 8000 });
  376. else
  377. new Toast({
  378. content: "Successfully changed location",
  379. timeout: 4000
  380. });
  381. }
  382. );
  383. },
  384. changeBio() {
  385. const { bio } = this.user;
  386. if (!validation.isLength(bio, 0, 200))
  387. return new Toast({
  388. content: "Bio must have between 0 and 200 characters.",
  389. timeout: 8000
  390. });
  391. return this.socket.emit(
  392. "users.updateBio",
  393. this.userId,
  394. bio,
  395. res => {
  396. if (res.status !== "success")
  397. new Toast({ content: res.message, timeout: 8000 });
  398. else
  399. new Toast({
  400. content: "Successfully changed bio",
  401. timeout: 4000
  402. });
  403. }
  404. );
  405. },
  406. changePassword() {
  407. const { newPassword } = this;
  408. if (!validation.isLength(newPassword, 6, 200))
  409. return new Toast({
  410. content: "Password must have between 6 and 200 characters.",
  411. timeout: 8000
  412. });
  413. if (!validation.regex.password.test(newPassword))
  414. return new Toast({
  415. content:
  416. "Invalid password format. Must have one lowercase letter, one uppercase letter, one number and one special character.",
  417. timeout: 8000
  418. });
  419. return this.socket.emit(
  420. "users.updatePassword",
  421. newPassword,
  422. res => {
  423. if (res.status !== "success")
  424. new Toast({ content: res.message, timeout: 8000 });
  425. else
  426. new Toast({
  427. content: "Successfully changed password",
  428. timeout: 4000
  429. });
  430. }
  431. );
  432. },
  433. requestPassword() {
  434. return this.socket.emit("users.requestPassword", res => {
  435. new Toast({ content: res.message, timeout: 8000 });
  436. if (res.status === "success") {
  437. this.passwordStep = 2;
  438. }
  439. });
  440. },
  441. verifyCode() {
  442. if (!this.passwordCode)
  443. return new Toast({
  444. content: "Code cannot be empty",
  445. timeout: 8000
  446. });
  447. return this.socket.emit(
  448. "users.verifyPasswordCode",
  449. this.passwordCode,
  450. res => {
  451. new Toast({ content: res.message, timeout: 8000 });
  452. if (res.status === "success") {
  453. this.passwordStep = 3;
  454. }
  455. }
  456. );
  457. },
  458. setPassword() {
  459. const newPassword = this.setNewPassword;
  460. if (!validation.isLength(newPassword, 6, 200))
  461. return new Toast({
  462. content: "Password must have between 6 and 200 characters.",
  463. timeout: 8000
  464. });
  465. if (!validation.regex.password.test(newPassword))
  466. return new Toast({
  467. content:
  468. "Invalid password format. Must have one lowercase letter, one uppercase letter, one number and one special character.",
  469. timeout: 8000
  470. });
  471. return this.socket.emit(
  472. "users.changePasswordWithCode",
  473. this.passwordCode,
  474. newPassword,
  475. res => {
  476. new Toast({ content: res.message, timeout: 8000 });
  477. }
  478. );
  479. },
  480. unlinkPassword() {
  481. this.socket.emit("users.unlinkPassword", res => {
  482. new Toast({ content: res.message, timeout: 8000 });
  483. });
  484. },
  485. unlinkGitHub() {
  486. this.socket.emit("users.unlinkGitHub", res => {
  487. new Toast({ content: res.message, timeout: 8000 });
  488. });
  489. },
  490. removeSessions() {
  491. this.socket.emit(`users.removeSessions`, this.userId, res => {
  492. new Toast({ content: res.message, timeout: 4000 });
  493. });
  494. },
  495. changeNightmodeLocal() {
  496. localStorage.setItem("nightmode", this.localNightmode);
  497. this.changeNightmode(this.localNightmode);
  498. },
  499. ...mapActions("user/preferences", ["changeNightmode"])
  500. }
  501. };
  502. </script>
  503. <style lang="scss" scoped>
  504. @import "styles/global.scss";
  505. .container {
  506. width: 962px;
  507. margin-left: auto;
  508. margin-right: auto;
  509. margin-top: 32px;
  510. padding: 24px;
  511. display: flex;
  512. .buttons {
  513. height: 100%;
  514. width: 250px;
  515. margin-right: 64px;
  516. button {
  517. outline: none;
  518. border: none;
  519. box-shadow: none;
  520. color: $musareBlue;
  521. font-size: 22px;
  522. line-height: 26px;
  523. padding: 7px 0 7px 12px;
  524. width: 100%;
  525. text-align: left;
  526. cursor: pointer;
  527. border-radius: 5px;
  528. background-color: transparent;
  529. &.active {
  530. color: $white;
  531. background-color: $musareBlue;
  532. }
  533. }
  534. }
  535. .content {
  536. width: 600px;
  537. .control {
  538. margin-bottom: 24px;
  539. }
  540. label {
  541. font-size: 14px;
  542. color: $dark-grey-2;
  543. padding-bottom: 4px;
  544. }
  545. input {
  546. height: 32px;
  547. }
  548. textarea {
  549. height: 96px;
  550. }
  551. input,
  552. textarea {
  553. border-radius: 3px;
  554. border: 1px solid $light-grey-2;
  555. }
  556. button {
  557. width: 100%;
  558. }
  559. .checkbox-control {
  560. input[type="checkbox"] {
  561. opacity: 0;
  562. position: absolute;
  563. }
  564. label span {
  565. cursor: pointer;
  566. width: 24px;
  567. height: 24px;
  568. background-color: $white;
  569. display: inline-block;
  570. border: 1px solid $dark-grey-2;
  571. position: relative;
  572. border-radius: 3px;
  573. }
  574. label p {
  575. margin-bottom: 4px;
  576. }
  577. input[type="checkbox"]:checked + label span::after {
  578. content: "";
  579. width: 18px;
  580. height: 18px;
  581. left: 2px;
  582. top: 2px;
  583. border-radius: 3px;
  584. background-color: $musareBlue;
  585. position: absolute;
  586. }
  587. }
  588. }
  589. }
  590. .night-mode {
  591. label {
  592. color: #ddd !important;
  593. }
  594. }
  595. </style>