ResetPassword.vue 13 KB

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