ResetPassword.vue 13 KB

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