Settings.vue 13 KB

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