ResetPassword.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. <template>
  2. <div>
  3. <metadata
  4. :title="mode === 'reset' ? 'Reset password' : 'Set password'"
  5. />
  6. <main-header />
  7. <div class="container">
  8. <div class="content-wrapper">
  9. <h1 id="title">
  10. {{ mode === "reset" ? "Reset" : "Set" }} your password
  11. </h1>
  12. <div id="steps">
  13. <p class="step" :class="{ selected: step === 1 }">1</p>
  14. <span class="divider"></span>
  15. <p class="step" :class="{ selected: step === 2 }">2</p>
  16. <span class="divider"></span>
  17. <p class="step" :class="{ selected: step === 3 }">3</p>
  18. </div>
  19. <transition name="steps-fade" mode="out-in">
  20. <!-- Step 1 -- Enter email address -->
  21. <div
  22. class="content-box"
  23. v-if="step === 1"
  24. v-bind:key="step"
  25. >
  26. <h2 class="content-box-title">
  27. Enter your email address
  28. </h2>
  29. <p class="content-box-description">
  30. We will send a code to your email address to verify
  31. your identity.
  32. </p>
  33. <p class="content-box-optional-helper">
  34. <a href="#" @click="step = 2"
  35. >Already have a code?</a
  36. >
  37. </p>
  38. <div class="content-box-inputs">
  39. <div class="control is-grouped input-with-button">
  40. <p class="control is-expanded">
  41. <input
  42. class="input"
  43. type="email"
  44. placeholder="Enter email address here..."
  45. autofocus
  46. v-model="email"
  47. @keyup.enter="submitEmail()"
  48. @blur="onInputBlur('email')"
  49. />
  50. </p>
  51. <p class="control">
  52. <a
  53. class="button is-info"
  54. href="#"
  55. @click="submitEmail()"
  56. ><i
  57. class="material-icons icon-with-button"
  58. >mail</i
  59. >Request</a
  60. >
  61. </p>
  62. </div>
  63. <p
  64. class="help"
  65. v-if="validation.email.entered"
  66. :class="
  67. validation.email.valid
  68. ? 'is-success'
  69. : 'is-danger'
  70. "
  71. >
  72. {{ validation.email.message }}
  73. </p>
  74. </div>
  75. </div>
  76. <!-- Step 2 -- Enter code -->
  77. <div
  78. class="content-box"
  79. v-if="step === 2"
  80. v-bind:key="step"
  81. >
  82. <h2 class="content-box-title">
  83. Enter the code sent to your email
  84. </h2>
  85. <p class="content-box-description">
  86. A code has been sent to <strong>email</strong>.
  87. </p>
  88. <p class="content-box-optional-helper">
  89. <a
  90. href="#"
  91. @click="email ? submitEmail() : (step = 1)"
  92. >Request another code</a
  93. >
  94. </p>
  95. <div class="content-box-inputs">
  96. <div class="control is-grouped input-with-button">
  97. <p class="control is-expanded">
  98. <input
  99. class="input"
  100. type="text"
  101. placeholder="Enter code here..."
  102. autofocus
  103. v-model="code"
  104. @keyup.enter="verifyCode()"
  105. />
  106. </p>
  107. <p class="control">
  108. <a
  109. class="button is-info"
  110. href="#"
  111. @click="verifyCode()"
  112. ><i
  113. class="material-icons icon-with-button"
  114. >vpn_key</i
  115. >Verify</a
  116. >
  117. </p>
  118. </div>
  119. </div>
  120. </div>
  121. <!-- Step 3 -- Set new password -->
  122. <div
  123. class="content-box"
  124. v-if="step === 3"
  125. v-bind:key="step"
  126. >
  127. <h2 class="content-box-title">
  128. Set a new password
  129. </h2>
  130. <p class="content-box-description">
  131. Create a new password for your account.
  132. </p>
  133. <div class="content-box-inputs">
  134. <p class="control is-expanded">
  135. <label for="new-password">New password</label>
  136. <input
  137. class="input"
  138. id="new-password"
  139. type="password"
  140. placeholder="Enter password here..."
  141. v-model="newPassword"
  142. @blur="onInputBlur('newPassword')"
  143. />
  144. </p>
  145. <p
  146. class="help"
  147. v-if="validation.newPassword.entered"
  148. :class="
  149. validation.newPassword.valid
  150. ? 'is-success'
  151. : 'is-danger'
  152. "
  153. >
  154. {{ validation.newPassword.message }}
  155. </p>
  156. <p
  157. id="new-password-again-input"
  158. class="control is-expanded"
  159. >
  160. <label for="new-password-again"
  161. >New password again</label
  162. >
  163. <input
  164. class="input"
  165. id="new-password-again"
  166. type="password"
  167. placeholder="Enter password here..."
  168. v-model="newPasswordAgain"
  169. @keyup.enter="changePassword()"
  170. @blur="onInputBlur('newPasswordAgain')"
  171. />
  172. </p>
  173. <p
  174. class="help"
  175. v-if="validation.newPasswordAgain.entered"
  176. :class="
  177. validation.newPasswordAgain.valid
  178. ? 'is-success'
  179. : 'is-danger'
  180. "
  181. >
  182. {{ validation.newPasswordAgain.message }}
  183. </p>
  184. <a
  185. id="change-password-button"
  186. class="button is-success"
  187. href="#"
  188. @click="changePassword()"
  189. >
  190. Change password</a
  191. >
  192. </div>
  193. </div>
  194. <div
  195. class="content-box reset-status-box"
  196. v-if="step === 4"
  197. v-bind:key="step"
  198. >
  199. <i class="material-icons success-icon">check_circle</i>
  200. <h2>Password successfully {{ mode }}</h2>
  201. <router-link
  202. class="button is-dark"
  203. href="#"
  204. to="/settings"
  205. ><i class="material-icons icon-with-button">undo</i
  206. >Return to Settings</router-link
  207. >
  208. </div>
  209. <div
  210. class="content-box reset-status-box"
  211. v-if="step === 5"
  212. v-bind:key="step"
  213. >
  214. <i class="material-icons error-icon">error</i>
  215. <h2>
  216. Password {{ mode }} failed, please try again later
  217. </h2>
  218. <router-link
  219. class="button is-dark"
  220. href="#"
  221. to="/settings"
  222. ><i class="material-icons icon-with-button">undo</i
  223. >Return to Settings</router-link
  224. >
  225. </div>
  226. </transition>
  227. </div>
  228. </div>
  229. <main-footer />
  230. </div>
  231. </template>
  232. <script>
  233. import Toast from "toasters";
  234. import MainHeader from "../components/layout/MainHeader.vue";
  235. import MainFooter from "../components/layout/MainFooter.vue";
  236. import io from "../io";
  237. import validation from "../validation";
  238. export default {
  239. components: { MainHeader, MainFooter },
  240. data() {
  241. return {
  242. email: "",
  243. code: "",
  244. newPassword: "",
  245. newPasswordAgain: "",
  246. validation: {
  247. email: {
  248. entered: false,
  249. valid: false,
  250. message: "Please enter a valid email address."
  251. },
  252. newPassword: {
  253. entered: false,
  254. valid: false,
  255. message: "Please enter a valid password."
  256. },
  257. newPasswordAgain: {
  258. entered: false,
  259. valid: false,
  260. message: "This password must match."
  261. }
  262. },
  263. step: 1
  264. };
  265. },
  266. props: {
  267. mode: {
  268. default: "reset",
  269. enum: ["reset", "set"],
  270. type: String
  271. }
  272. },
  273. mounted() {
  274. io.getSocket(socket => {
  275. this.socket = socket;
  276. });
  277. },
  278. watch: {
  279. email(value) {
  280. if (
  281. value.indexOf("@") !== value.lastIndexOf("@") ||
  282. !validation.regex.emailSimple.test(value)
  283. ) {
  284. this.validation.email.message =
  285. "Please enter a valid email address.";
  286. this.validation.email.valid = false;
  287. } else {
  288. this.validation.email.message = "Everything looks great!";
  289. this.validation.email.valid = true;
  290. }
  291. },
  292. newPassword(value) {
  293. this.checkPasswordMatch(value, this.newPasswordAgain);
  294. if (!validation.isLength(value, 6, 200)) {
  295. this.validation.newPassword.message =
  296. "Password must have between 6 and 200 characters.";
  297. this.validation.newPassword.valid = false;
  298. } else if (!validation.regex.password.test(value)) {
  299. this.validation.newPassword.message =
  300. "Invalid password format. Must have one lowercase letter, one uppercase letter, one number and one special character.";
  301. this.validation.newPassword.valid = false;
  302. } else {
  303. this.validation.newPassword.message = "Everything looks great!";
  304. this.validation.newPassword.valid = true;
  305. }
  306. },
  307. newPasswordAgain(value) {
  308. this.checkPasswordMatch(this.newPassword, value);
  309. }
  310. },
  311. methods: {
  312. checkPasswordMatch(newPassword, newPasswordAgain) {
  313. if (newPasswordAgain !== newPassword) {
  314. this.validation.newPasswordAgain.message =
  315. "This password must match.";
  316. this.validation.newPasswordAgain.valid = false;
  317. } else {
  318. this.validation.newPasswordAgain.message =
  319. "Everything looks great!";
  320. this.validation.newPasswordAgain.valid = true;
  321. }
  322. },
  323. onInputBlur(inputName) {
  324. this.validation[inputName].entered = true;
  325. },
  326. submitEmail() {
  327. if (
  328. this.email.indexOf("@") !== this.email.lastIndexOf("@") ||
  329. !validation.regex.emailSimple.test(this.email)
  330. )
  331. return new Toast({
  332. content: "Invalid email format.",
  333. timeout: 8000
  334. });
  335. if (!this.email)
  336. return new Toast({
  337. content: "Email cannot be empty",
  338. timeout: 8000
  339. });
  340. if (this.mode === "set") {
  341. return this.socket.emit("users.requestPassword", res => {
  342. new Toast({ content: res.message, timeout: 8000 });
  343. if (res.status === "success") {
  344. this.step = 2;
  345. }
  346. });
  347. }
  348. return this.socket.emit(
  349. "users.requestPasswordReset",
  350. this.email,
  351. res => {
  352. new Toast({ content: res.message, timeout: 8000 });
  353. if (res.status === "success") {
  354. this.code = ""; // in case: already have a code -> request another code
  355. this.step = 2;
  356. } else this.step = 5;
  357. }
  358. );
  359. },
  360. verifyCode() {
  361. if (!this.code)
  362. return new Toast({
  363. content: "Code cannot be empty",
  364. timeout: 8000
  365. });
  366. return this.socket.emit(
  367. this.mode === "set"
  368. ? "users.verifyPasswordCode"
  369. : "users.verifyPasswordResetCode",
  370. this.code,
  371. res => {
  372. new Toast({ content: res.message, timeout: 8000 });
  373. if (res.status === "success") {
  374. this.step = 3;
  375. }
  376. }
  377. );
  378. },
  379. changePassword() {
  380. if (
  381. this.validation.newPassword.valid &&
  382. !this.validation.newPasswordAgain.valid
  383. )
  384. return new Toast({
  385. content: "Please ensure the passwords match.",
  386. timeout: 8000
  387. });
  388. if (!this.validation.newPassword.valid)
  389. return new Toast({
  390. content: "Please enter a valid password.",
  391. timeout: 8000
  392. });
  393. return this.socket.emit(
  394. this.mode === "set"
  395. ? "users.changePasswordWithCode"
  396. : "users.changePasswordWithResetCode",
  397. this.code,
  398. this.newPassword,
  399. res => {
  400. new Toast({ content: res.message, timeout: 8000 });
  401. if (res.status === "success") this.step = 4;
  402. else this.step = 5;
  403. }
  404. );
  405. }
  406. }
  407. };
  408. </script>
  409. <style lang="scss" scoped>
  410. @import "../styles/global.scss";
  411. .night-mode {
  412. .label {
  413. color: #ddd;
  414. }
  415. .skip-step {
  416. border: 0;
  417. }
  418. }
  419. h1,
  420. h2,
  421. p {
  422. margin: 0;
  423. }
  424. .help {
  425. margin-bottom: 5px;
  426. }
  427. .container {
  428. padding: 25px;
  429. #title {
  430. color: #000;
  431. font-size: 42px;
  432. text-align: center;
  433. }
  434. #steps {
  435. display: flex;
  436. align-items: center;
  437. justify-content: center;
  438. height: 50px;
  439. margin-top: 36px;
  440. @media screen and (max-width: 300px) {
  441. display: none;
  442. }
  443. .step {
  444. display: flex;
  445. align-items: center;
  446. justify-content: center;
  447. border-radius: 100%;
  448. border: 1px solid $dark-grey;
  449. min-width: 50px;
  450. min-height: 50px;
  451. background-color: #fff;
  452. font-size: 30px;
  453. cursor: pointer;
  454. &.selected {
  455. background-color: $musare-blue;
  456. color: #fff;
  457. border: 0;
  458. }
  459. }
  460. .divider {
  461. display: flex;
  462. justify-content: center;
  463. width: 180px;
  464. height: 1px;
  465. background-color: $dark-grey;
  466. }
  467. }
  468. .content-box {
  469. margin-top: 90px;
  470. border-radius: 3px;
  471. background-color: #fff;
  472. border: 1px solid $dark-grey;
  473. max-width: 580px;
  474. padding: 40px;
  475. @media screen and (max-width: 300px) {
  476. margin-top: 30px;
  477. padding: 30px 20px;
  478. }
  479. .content-box-title {
  480. font-size: 25px;
  481. color: #000;
  482. }
  483. .content-box-description {
  484. font-size: 14px;
  485. color: $dark-grey;
  486. }
  487. .content-box-optional-helper {
  488. margin-top: 15px;
  489. color: $musare-blue;
  490. text-decoration: underline;
  491. font-size: 16px;
  492. }
  493. .content-box-inputs {
  494. margin-top: 35px;
  495. .input-with-button {
  496. .button {
  497. width: 105px;
  498. }
  499. @media screen and (max-width: 450px) {
  500. flex-direction: column;
  501. }
  502. }
  503. label {
  504. font-size: 11px;
  505. }
  506. #change-password-button {
  507. margin-top: 36px;
  508. width: 175px;
  509. }
  510. }
  511. }
  512. .reset-status-box {
  513. display: flex;
  514. flex-direction: column;
  515. align-items: center;
  516. justify-content: center;
  517. height: 356px;
  518. h2 {
  519. margin-top: 10px;
  520. font-size: 21px;
  521. font-weight: 800;
  522. color: #000;
  523. text-align: center;
  524. }
  525. .success-icon {
  526. color: #24a216;
  527. }
  528. .error-icon {
  529. color: $red;
  530. }
  531. .success-icon,
  532. .error-icon {
  533. font-size: 125px;
  534. }
  535. .button {
  536. margin-top: 36px;
  537. }
  538. }
  539. }
  540. .steps-fade-enter-active,
  541. .steps-fade-leave-active {
  542. transition: all 0.3s ease;
  543. }
  544. .steps-fade-enter,
  545. .steps-fade-leave-to {
  546. opacity: 0;
  547. }
  548. .skip-step {
  549. background-color: #7e7e7e;
  550. color: $white;
  551. }
  552. </style>