ResetPassword.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  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. :ref="
  219. el =>
  220. (inputElements[
  221. 'email'
  222. ] = el)
  223. "
  224. placeholder="Enter email address here..."
  225. autofocus
  226. v-model="inputs.email.value"
  227. @keyup.enter="submitEmail()"
  228. @keypress="onInput('email')"
  229. @paste="onInput('email')"
  230. />
  231. </p>
  232. <p class="control">
  233. <button
  234. class="button is-info"
  235. @click="submitEmail()"
  236. >
  237. <i
  238. class="material-icons icon-with-button"
  239. >mail</i
  240. >Request
  241. </button>
  242. </p>
  243. </div>
  244. <transition name="fadein-helpbox">
  245. <input-help-box
  246. :entered="inputs.email.entered"
  247. :valid="inputs.email.valid"
  248. :message="inputs.email.message"
  249. />
  250. </transition>
  251. </div>
  252. </div>
  253. <!-- Step 2 -- Enter code -->
  254. <div v-if="step === 2" key="2">
  255. <h2 class="content-box-title">
  256. Enter the code sent to your email
  257. </h2>
  258. <p
  259. class="content-box-description"
  260. v-if="!inputs.email.hasBeenSentAlready"
  261. >
  262. A code has been sent to
  263. <strong>{{ inputs.email.value }}.</strong>
  264. </p>
  265. <p class="content-box-optional-helper">
  266. <a
  267. @click="
  268. inputs.email.value
  269. ? submitEmail()
  270. : (step = 1)
  271. "
  272. >Request another code</a
  273. >
  274. </p>
  275. <div class="content-box-inputs">
  276. <div
  277. class="control is-grouped input-with-button"
  278. >
  279. <p class="control is-expanded">
  280. <input
  281. class="input"
  282. type="text"
  283. placeholder="Enter code here..."
  284. autofocus
  285. v-model="code"
  286. @keyup.enter="verifyCode()"
  287. />
  288. </p>
  289. <p class="control">
  290. <button
  291. class="button is-info"
  292. @click="verifyCode()"
  293. >
  294. <i
  295. class="material-icons icon-with-button"
  296. >vpn_key</i
  297. >Verify
  298. </button>
  299. </p>
  300. </div>
  301. </div>
  302. </div>
  303. <!-- Step 3 -- Set new password -->
  304. <div v-if="step === 3" key="3">
  305. <h2 class="content-box-title">
  306. Set a new password
  307. </h2>
  308. <p class="content-box-description">
  309. Create a new password for your account.
  310. </p>
  311. <div class="content-box-inputs">
  312. <p class="control is-expanded">
  313. <label for="new-password"
  314. >New password</label
  315. >
  316. </p>
  317. <div id="password-visibility-container">
  318. <input
  319. class="input"
  320. id="new-password"
  321. type="password"
  322. :ref="
  323. el =>
  324. (inputElements['password'] =
  325. el)
  326. "
  327. placeholder="Enter password here..."
  328. v-model="inputs.password.value"
  329. @keypress="onInput('password')"
  330. @paste="onInput('password')"
  331. />
  332. <a
  333. @click="
  334. togglePasswordVisibility(
  335. 'password'
  336. )
  337. "
  338. >
  339. <i class="material-icons">
  340. {{
  341. !inputs.password.visible
  342. ? "visibility"
  343. : "visibility_off"
  344. }}
  345. </i>
  346. </a>
  347. </div>
  348. <transition name="fadein-helpbox">
  349. <input-help-box
  350. :entered="inputs.password.entered"
  351. :valid="inputs.password.valid"
  352. :message="inputs.password.message"
  353. />
  354. </transition>
  355. <p
  356. id="new-password-again-input"
  357. class="control is-expanded"
  358. >
  359. <label for="new-password-again"
  360. >New password again</label
  361. >
  362. </p>
  363. <div id="password-visibility-container">
  364. <input
  365. class="input"
  366. id="new-password-again"
  367. type="password"
  368. :ref="
  369. el =>
  370. (inputElements[
  371. 'passwordAgain'
  372. ] = el)
  373. "
  374. placeholder="Enter password here..."
  375. v-model="inputs.passwordAgain.value"
  376. @keyup.enter="changePassword()"
  377. @keypress="onInput('passwordAgain')"
  378. @paste="onInput('passwordAgain')"
  379. />
  380. <a
  381. @click="
  382. togglePasswordVisibility(
  383. 'passwordAgain'
  384. )
  385. "
  386. >
  387. <i class="material-icons">
  388. {{
  389. !inputs.passwordAgain
  390. .visible
  391. ? "visibility"
  392. : "visibility_off"
  393. }}
  394. </i>
  395. </a>
  396. </div>
  397. <transition name="fadein-helpbox">
  398. <input-help-box
  399. :entered="
  400. inputs.passwordAgain.entered
  401. "
  402. :valid="inputs.passwordAgain.valid"
  403. :message="
  404. inputs.passwordAgain.message
  405. "
  406. />
  407. </transition>
  408. <button
  409. id="change-password-button"
  410. class="button is-success"
  411. @click="changePassword()"
  412. >
  413. Change password
  414. </button>
  415. </div>
  416. </div>
  417. <div
  418. class="reset-status-box"
  419. v-if="step === 4"
  420. key="4"
  421. >
  422. <i class="material-icons success-icon"
  423. >check_circle</i
  424. >
  425. <h2>Password successfully {{ mode }}</h2>
  426. <router-link
  427. class="button is-dark"
  428. to="/settings"
  429. ><i class="material-icons icon-with-button"
  430. >undo</i
  431. >Return to Settings</router-link
  432. >
  433. </div>
  434. <div
  435. class="reset-status-box"
  436. v-if="step === 5"
  437. key="5"
  438. >
  439. <i class="material-icons error-icon">error</i>
  440. <h2>
  441. Password {{ mode }} failed, please try again
  442. later
  443. </h2>
  444. <router-link
  445. class="button is-dark"
  446. to="/settings"
  447. ><i class="material-icons icon-with-button"
  448. >undo</i
  449. >Return to Settings</router-link
  450. >
  451. </div>
  452. </div>
  453. </transition-group>
  454. </div>
  455. </div>
  456. </div>
  457. <main-footer />
  458. </div>
  459. </template>
  460. <style lang="less" scoped>
  461. .night-mode {
  462. .label {
  463. color: var(--light-grey-2);
  464. }
  465. .skip-step {
  466. border: 0;
  467. }
  468. }
  469. h1,
  470. h2,
  471. p {
  472. margin: 0;
  473. }
  474. .content-wrapper {
  475. display: flex;
  476. flex-direction: column;
  477. align-items: center;
  478. }
  479. .container {
  480. padding: 25px;
  481. #title {
  482. color: var(--black);
  483. font-size: 42px;
  484. }
  485. .reset-status-box {
  486. display: flex;
  487. flex-direction: column;
  488. align-items: center;
  489. justify-content: center;
  490. height: 356px;
  491. h2 {
  492. margin-top: 10px;
  493. font-size: 21px;
  494. font-weight: 800;
  495. color: var(--black);
  496. text-align: center;
  497. }
  498. .success-icon {
  499. color: var(--green);
  500. }
  501. .error-icon {
  502. color: var(--dark-red);
  503. }
  504. .success-icon,
  505. .error-icon {
  506. font-size: 125px;
  507. }
  508. .button {
  509. margin-top: 36px;
  510. }
  511. }
  512. }
  513. .control {
  514. margin-bottom: 2px !important;
  515. }
  516. </style>