Explorar el Código

feat: adiciona logica de recuperar a senha

ebagabee hace 1 mes
padre
commit
2c25fe0da4

+ 21 - 0
src/api/auth.js

@@ -0,0 +1,21 @@
+import api from "src/api";
+
+export const forgotPassword = async (email) => {
+  const { data } = await api.post("/forgot-password", { email });
+  return data;
+};
+
+export const verifyPasswordCode = async (email, code) => {
+  const { data } = await api.post("/forgot-password/verify-code", { email, code });
+  return data;
+};
+
+export const resetPassword = async (email, code, password, passwordConfirmation) => {
+  const { data } = await api.post("/reset-password", {
+    email,
+    code,
+    password,
+    password_confirmation: passwordConfirmation,
+  });
+  return data;
+};

+ 1 - 0
src/composables/useAuth.js

@@ -15,6 +15,7 @@ export const useAuth = () => {
       const response = await api.post("/login", {
         email: email,
         password: password,
+        origem: 'franchisee'
       });
 
       if (response.status === 200) {

+ 21 - 8
src/pages/ForgotMyPasswordPage.vue

@@ -24,7 +24,9 @@
       <EmailDataStep
         v-if="currentStep === 'email-data'"
         :loading
+        :email-error="emailError"
         @submit="handleSubmit"
+        @clear-error="emailError = ''"
       />
 
       <ResendEmailStep
@@ -41,25 +43,36 @@
 import { ref } from "vue";
 import EmailDataStep from "./forgot-password/EmailDataStep.vue";
 import ResendEmailStep from "./forgot-password/ResendEmailStep.vue";
+import { forgotPassword } from "src/api/auth";
 
 const currentStep = ref("email-data");
 const email = ref("");
 const loading = ref(false);
+const emailError = ref("");
 
 async function handleSubmit(submittedEmail) {
   loading.value = true;
-  email.value = submittedEmail;
-  // TODO: chamar API de recuperação de senha
-  await new Promise((resolve) => setTimeout(resolve, 500));
-  loading.value = false;
-  currentStep.value = "resend-email";
+  emailError.value = "";
+  try {
+    await forgotPassword(submittedEmail);
+    email.value = submittedEmail;
+    currentStep.value = "resend-email";
+  } catch (err) {
+    if (err.response?.status === 422) {
+      emailError.value = err.response.data.message;
+    }
+  } finally {
+    loading.value = false;
+  }
 }
 
 async function handleResend() {
   loading.value = true;
-  // TODO: chamar API de recuperação de senha
-  await new Promise((resolve) => setTimeout(resolve, 500));
-  loading.value = false;
+  try {
+    await forgotPassword(email.value);
+  } finally {
+    loading.value = false;
+  }
 }
 </script>
 

+ 96 - 0
src/pages/RecoveryPasswordPage.vue

@@ -0,0 +1,96 @@
+<template>
+  <q-page class="column justify-center items-center login-page">
+    <div
+      flat
+      class="column justify-center items-center full-width z-top bg-background q-py-xl"
+      style="max-width: 659px; border-radius: 15px;"
+    >
+      <div class="flex full-width q-gutter-x-sm items-center q-mb-xl">
+        <q-icon
+          name="mdi-arrow-left"
+          size="sm"
+          color="primary"
+          @click="handleBack"
+        />
+        <span class="text-h6 text-weight-regular">Recuperar senha</span>
+      </div>
+
+      <VerifyCodeStep
+        v-if="currentStep === 'verify-code'"
+        :loading
+        @submit="handleVerify"
+        @resend="handleResend"
+      />
+
+      <NewPasswordStep
+        v-else-if="currentStep === 'new-password'"
+        :loading
+        @submit="handleReset"
+      />
+    </div>
+  </q-page>
+</template>
+
+<script setup>
+import { ref } from "vue";
+import { useRoute, useRouter } from "vue-router";
+import VerifyCodeStep from "./forgot-password/VerifyCodeStep.vue";
+import NewPasswordStep from "./forgot-password/NewPasswordStep.vue";
+import { forgotPassword, verifyPasswordCode, resetPassword } from "src/api/auth";
+
+const route = useRoute();
+const router = useRouter();
+
+const email = route.query.email ?? "";
+const currentStep = ref("verify-code");
+const verifiedCode = ref("");
+const loading = ref(false);
+
+function handleBack() {
+  if (currentStep.value === "new-password") {
+    currentStep.value = "verify-code";
+  } else {
+    router.push("/forgot-my-password");
+  }
+}
+
+async function handleVerify(code) {
+  loading.value = true;
+  try {
+    await verifyPasswordCode(email, code);
+    verifiedCode.value = code;
+    await new Promise((resolve) => setTimeout(resolve, 3000));
+    currentStep.value = "new-password";
+  } finally {
+    loading.value = false;
+  }
+}
+
+async function handleResend() {
+  loading.value = true;
+  try {
+    await forgotPassword(email);
+  } finally {
+    loading.value = false;
+  }
+}
+
+async function handleReset({ password, passwordConfirmation }) {
+  loading.value = true;
+  try {
+    await resetPassword(email, verifiedCode.value, password, passwordConfirmation);
+    router.push("/login");
+  } finally {
+    loading.value = false;
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.login-page {
+  background-image: url("/images/background.png");
+  background-size: cover;
+  background-repeat: no-repeat;
+  background-position: center;
+}
+</style>

+ 5 - 1
src/pages/forgot-password/EmailDataStep.vue

@@ -25,6 +25,9 @@
         lazy-rules
         :label="$t('common.terms.email')"
         :rules="[inputRules.required]"
+        :error="!!emailError"
+        :error-message="emailError"
+        @update:model-value="emit('clear-error')"
       />
 
       <div>
@@ -54,9 +57,10 @@ import { ref } from "vue";
 
 defineProps({
   loading: Boolean,
+  emailError: { type: String, default: "" },
 });
 
-const emit = defineEmits(["submit"]);
+const emit = defineEmits(["submit", "clear-error"]);
 
 const email = ref("");
 const { inputRules } = useInputRules();

+ 71 - 0
src/pages/forgot-password/NewPasswordStep.vue

@@ -0,0 +1,71 @@
+<template>
+  <div class="column items-center full-width">
+    <q-img
+      :src="Logo"
+      style="max-width: 379px; max-height: 106px; height: 100%; width: 100%"
+    />
+
+    <div class="text-h5 text-weight-regular q-mt-lg text-primary text-center">
+      Definir nova senha
+    </div>
+
+    <div
+      class="items-center full-width q-px-lg q-mt-lg"
+      style="display: flex; gap: 12px"
+    >
+      <div class="bg-primary" style="height: 1px; flex: 1"></div>
+      <q-icon name="mdi-chevron-down" size="lg" color="warning" />
+      <div class="bg-primary" style="height: 1px; flex: 1"></div>
+    </div>
+
+    <q-form
+      class="full-width q-px-lg column q-gutter-y-md q-mt-md"
+      @submit.prevent="emit('submit', { password, passwordConfirmation })"
+    >
+      <DefaultPasswordInput
+        v-model="password"
+        label="Nova senha"
+        lazy-rules
+        :rules="[inputRules.required, inputRules.min(8)]"
+      />
+
+      <DefaultPasswordInput
+        v-model="passwordConfirmation"
+        label="Confirmar nova senha"
+        lazy-rules
+        :rules="[inputRules.required, (v) => v === password || 'As senhas não conferem']"
+      />
+
+      <q-btn
+        class="full-width q-mt-sm"
+        color="primary-2"
+        label="Confirmar"
+        size="md"
+        padding="sm"
+        type="submit"
+        :loading
+      >
+        <template #loading>
+          <q-spinner />
+        </template>
+      </q-btn>
+    </q-form>
+  </div>
+</template>
+
+<script setup>
+import Logo from "src/assets/images/logo.svg";
+import DefaultPasswordInput from "src/components/defaults/DefaultPasswordInput.vue";
+import { useInputRules } from "src/composables/useInputRules";
+import { ref } from "vue";
+
+defineProps({
+  loading: Boolean,
+});
+
+const emit = defineEmits(["submit"]);
+
+const password = ref("");
+const passwordConfirmation = ref("");
+const { inputRules } = useInputRules();
+</script>

+ 1 - 1
src/pages/forgot-password/ResendEmailStep.vue

@@ -22,7 +22,7 @@
       style="font-weight: 400"
     >
       Enviamos um e-mail com um link para redefinir sua senha<br />
-      Por Favor confira sua caixa de entrada e clique no link para prosseguir
+      Por favor confira sua caixa de entrada e clique no link para prosseguir
     </div>
 
     <div class="text-body1 text-weight-medium text-primary q-mt-md">

+ 138 - 0
src/pages/forgot-password/VerifyCodeStep.vue

@@ -0,0 +1,138 @@
+<template>
+  <div class="column items-center full-width">
+    <q-img
+      :src="Logo"
+      style="max-width: 379px; max-height: 106px; height: 100%; width: 100%"
+    />
+
+    <div class="text-h5 text-weight-regular q-mt-lg text-primary text-center">
+      Digite o código de verificação
+    </div>
+
+    <div
+      class="items-center full-width q-px-lg q-mt-lg"
+      style="display: flex; gap: 12px"
+    >
+      <div class="bg-primary" style="height: 1px; flex: 1"></div>
+      <q-icon name="mdi-chevron-down" size="lg" color="warning" />
+      <div class="bg-primary" style="height: 1px; flex: 1"></div>
+    </div>
+
+    <div
+      class="text-body2 text-center text-primary q-px-lg q-mt-md"
+      style="font-weight: 400"
+    >
+      Enviamos um código de 6 dígitos para seu e-mail<br />
+      Digite o código para verificar
+    </div>
+
+    <div class="row q-gutter-x-sm q-mt-xl justify-center">
+      <input
+        v-for="(_, index) in 6"
+        :key="index"
+        :ref="(el) => (inputs[index] = el)"
+        v-model="digits[index]"
+        type="text"
+        inputmode="numeric"
+        maxlength="1"
+        class="otp-input text-primary text-h5 text-center text-weight-medium"
+        :disabled="loading"
+        @input="handleInput(index)"
+        @keydown="handleKeydown(index, $event)"
+        @paste="handlePaste"
+      />
+    </div>
+
+    <div class="full-width q-px-lg q-mt-xl">
+      <q-btn
+        class="full-width"
+        color="primary-2"
+        size="md"
+        padding="sm"
+        :loading
+        @click="emit('resend')"
+      >
+        <q-icon name="mdi-refresh" class="q-mr-sm" />
+        Reenviar E-mail
+        <template #loading>
+          <q-spinner />
+        </template>
+      </q-btn>
+    </div>
+
+    <div class="q-mt-md text-body2 text-primary">
+      Lembrou sua senha?
+      <router-link to="/login" class="text-warning text-weight-medium" style="text-decoration: none">
+        Faça seu Login
+      </router-link>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import Logo from "src/assets/images/logo.svg";
+import { ref } from "vue";
+
+defineProps({
+  loading: Boolean,
+});
+
+const emit = defineEmits(["submit", "resend"]);
+
+const digits = ref(["", "", "", "", "", ""]);
+const inputs = ref([]);
+
+function handleInput(index) {
+  const val = digits.value[index].replace(/\D/g, "").slice(-1);
+  digits.value[index] = val;
+
+  if (val && index < 5) {
+    inputs.value[index + 1]?.focus();
+  }
+
+  const code = digits.value.join("");
+  if (code.length === 6) {
+    emit("submit", code);
+  }
+}
+
+function handleKeydown(index, event) {
+  if (event.key === "Backspace" && !digits.value[index] && index > 0) {
+    inputs.value[index - 1]?.focus();
+  }
+}
+
+function handlePaste(event) {
+  event.preventDefault();
+  const text = event.clipboardData.getData("text").replace(/\D/g, "").slice(0, 6);
+  text.split("").forEach((char, i) => {
+    digits.value[i] = char;
+  });
+  const nextIndex = Math.min(text.length, 5);
+  inputs.value[nextIndex]?.focus();
+
+  if (text.length === 6) {
+    emit("submit", text);
+  }
+}
+</script>
+
+<style scoped>
+.otp-input {
+  width: 44px;
+  height: 52px;
+  border: none;
+  border-bottom: 2px solid #1a5c38;
+  background: transparent;
+  outline: none;
+  font-size: 1.5rem;
+}
+
+.otp-input:focus {
+  border-bottom-color: #f5a623;
+}
+
+.otp-input:disabled {
+  opacity: 0.5;
+}
+</style>

+ 3 - 1
src/router/index.js

@@ -43,7 +43,9 @@ export default defineRouter(function (/* { store, ssrContext } */) {
         await useAuth().refresh();
       } catch {
         refreshed = true;
-        return next({ name: "LoginPage" });
+        if (!to.meta.public) {
+          return next({ name: "LoginPage" });
+        }
       }
     }
     if (userStore().accessToken) {

+ 16 - 0
src/router/routes.js

@@ -84,11 +84,27 @@ const routes = [
   {
     path: "/forgot-my-password",
     component: () => import("layouts/LoginLayout.vue"),
+    meta: { public: true },
     children: [
       {
         path: "",
         name: "ForgotMyPasswordPage",
         component: () => import("pages/ForgotMyPasswordPage.vue"),
+        meta: { public: true },
+      },
+    ],
+  },
+
+  {
+    path: "/recovery-password",
+    component: () => import("layouts/LoginLayout.vue"),
+    meta: { public: true },
+    children: [
+      {
+        path: "",
+        name: "RecoveryPasswordPage",
+        component: () => import("pages/RecoveryPasswordPage.vue"),
+        meta: { public: true },
       },
     ],
   },