| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259 |
- <script setup>
- import { computed, onMounted, onUnmounted, ref, watch } from "vue";
- import { useRoute, useRouter } from "vue-router";
- import BotaoAlterarTema from "../../components/BotaoAlterarTema.vue";
- import DecorativeGradient from "../../components/DecorativeGradient.vue";
- import ModalEsqueciSenha from "../../components/ModalEsqueciSenha.vue";
- import BaseButton from "../../components/base/BaseButton.vue";
- import BaseIcon from "../../components/base/BaseIcon.vue";
- import Logo from "../../components/base/Logo.vue";
- import { LocateFixed, Eye, EyeOff } from "lucide-vue-next";
- import { useAuth } from "../../composables/useAuth.js";
- const router = useRouter();
- const route = useRoute();
- const { login } = useAuth();
- const form = ref({
- login: "",
- senha: "",
- rememberMe: false
- });
- const loading = ref(false);
- const errorCategory = ref("");
- const retryAfter = ref(0);
- const senhaVisivel = ref(false);
- const capsLockOn = ref(false);
- const loginInputRef = ref(null);
- const modalEsqueciSenha = ref(false);
- let countdownTimer = null;
- const ERROR_MESSAGES = {
- invalid_credentials: "Usuário ou senha incorretos. Confira os dados e tente novamente.",
- network: "Não foi possível conectar ao servidor. Verifique sua conexão e tente novamente.",
- server: "O servidor está indisponível no momento. Tente novamente em instantes."
- };
- onMounted(() => {
- loginInputRef.value?.focus();
- });
- onUnmounted(() => {
- clearInterval(countdownTimer);
- });
- const bloqueado = computed(() => retryAfter.value > 0);
- const tempoRestante = computed(() => {
- const min = Math.floor(retryAfter.value / 60);
- const seg = retryAfter.value % 60;
- return `${String(min).padStart(2, "0")}:${String(seg).padStart(2, "0")}`;
- });
- const errorMessage = computed(() => {
- if (!errorCategory.value) return "";
- if (errorCategory.value === "locked") {
- return bloqueado.value
- ? `Muitas tentativas incorretas. Tente novamente em ${tempoRestante.value}.`
- : "Muitas tentativas incorretas. Aguarde alguns minutos e tente novamente.";
- }
- return ERROR_MESSAGES[errorCategory.value] ?? ERROR_MESSAGES.server;
- });
- const canSubmit = computed(() => {
- return Boolean(form.value.login.trim() && form.value.senha) && !bloqueado.value;
- });
- watch(() => [form.value.login, form.value.senha], () => {
-
- if (errorCategory.value && errorCategory.value !== "locked") errorCategory.value = "";
- });
- function startCountdown(seconds) {
- clearInterval(countdownTimer);
- retryAfter.value = seconds;
- countdownTimer = setInterval(() => {
- retryAfter.value -= 1;
- if (retryAfter.value <= 0) {
- clearInterval(countdownTimer);
- countdownTimer = null;
- retryAfter.value = 0;
- errorCategory.value = "";
- }
- }, 1000);
- }
- function onSenhaKeyEvent(event) {
- capsLockOn.value = event.getModifierState?.("CapsLock") ?? false;
- }
- async function onSubmit() {
- if (!canSubmit.value || loading.value) return;
- loading.value = true;
- errorCategory.value = "";
- try {
- await login(form.value);
- const raw = typeof route.query.redirect === "string" ? route.query.redirect : "";
- const redirect = raw.startsWith("/") && !raw.startsWith("//") ? raw : "/";
- await router.replace(redirect);
- } catch (err) {
- errorCategory.value = err?.category ?? "server";
- if (errorCategory.value === "locked" && Number(err?.retryAfterSeconds) > 0) {
- startCountdown(Number(err.retryAfterSeconds));
- }
- } finally {
- loading.value = false;
- }
- }
- </script>
- <template>
- <div class="relative min-h-screen overflow-hidden bg-background text-foreground">
- <DecorativeGradient />
- <header class="relative z-10 flex items-center justify-between px-6 py-5">
- <div>
- <Logo />
- <div class="mt-1 text-sm text-muted-foreground">
- Acesso ao assistente interno
- </div>
- </div>
- <BotaoAlterarTema />
- </header>
- <main class="relative z-10 flex min-h-[calc(100vh-88px)] items-center justify-center px-4 pb-8">
- <div
- class="w-full max-w-md rounded-[28px] border border-gray-300 bg-white/80 p-6 shadow-sm backdrop-blur-md dark:border-white/[0.18] dark:bg-gradient-to-b dark:from-abyss-900 dark:to-abyss-950 dark:shadow-[0_18px_50px_rgba(0,0,0,0.35)]"
- >
- <div class="flex items-center gap-3">
- <div
- aria-hidden="true"
- class="flex h-11 w-11 items-center justify-center rounded-2xl bg-primary/10 text-primary dark:bg-primary/20"
- >
- <BaseIcon :icon="LocateFixed" class="h-6 w-6" />
- </div>
- <div>
- <div class="text-2xl font-extrabold tracking-tight">Entrar</div>
- <div class="text-sm text-muted-foreground">
- Informe seu usuario ou email e sua senha para acessar o sistema.
- </div>
- </div>
- </div>
- <form class="mt-6 grid gap-4" @submit.prevent="onSubmit">
- <label class="grid gap-1.5">
- <span class="text-sm font-medium">Usuario ou email</span>
- <input
- ref="loginInputRef"
- v-model="form.login"
- type="text"
- name="username"
- autocomplete="username"
- placeholder="Digite seu login"
- required
- aria-required="true"
- :aria-invalid="errorMessage ? 'true' : undefined"
- :aria-describedby="errorMessage ? 'login-error' : undefined"
- />
- </label>
- <label class="grid gap-1.5">
- <span class="text-sm font-medium">Senha</span>
- <div class="relative">
- <input
- v-model="form.senha"
- :type="senhaVisivel ? 'text' : 'password'"
- name="password"
- autocomplete="current-password"
- placeholder="Digite sua senha"
- required
- aria-required="true"
- :aria-invalid="errorMessage ? 'true' : undefined"
- :aria-describedby="errorMessage ? 'login-error' : undefined"
- class="pr-10"
- @keydown="onSenhaKeyEvent"
- @keyup="onSenhaKeyEvent"
- @blur="capsLockOn = false"
- />
- <button
- type="button"
- class="absolute inset-y-0 right-0 flex items-center border-0 bg-transparent px-3 text-gray-400 hover:text-muted-foreground dark:hover:text-gray-300"
- :aria-label="senhaVisivel ? 'Ocultar senha' : 'Mostrar senha'"
- @click="senhaVisivel = !senhaVisivel"
- >
- <BaseIcon v-if="!senhaVisivel" :icon="Eye" class="h-4 w-4" />
- <BaseIcon v-else :icon="EyeOff" class="h-4 w-4" />
- </button>
- </div>
- <p
- v-if="capsLockOn"
- role="status"
- class="text-xs font-medium text-amber-600 dark:text-amber-400"
- >
- Caps Lock está ativado
- </p>
- </label>
- <div class="flex items-center justify-between gap-2">
- <label class="flex items-center gap-2 text-sm text-muted-foreground">
- <input
- v-model="form.rememberMe"
- type="checkbox"
- class="!h-4 !w-4 rounded border-gray-300 !p-0"
- />
- <span>Manter conectado neste navegador</span>
- </label>
- <button
- type="button"
- class="border-0 bg-transparent text-sm font-medium text-primary hover:underline"
- @click="modalEsqueciSenha = true"
- >
- Esqueci minha senha
- </button>
- </div>
- <Transition name="error-banner">
- <div
- v-if="errorMessage"
- id="login-error"
- role="alert"
- class="rounded-xl border border-red-300 bg-red-50 px-3 py-2 text-sm text-red-700 dark:border-red-500/40 dark:bg-red-500/10 dark:text-red-200"
- >
- {{ errorMessage }}
- </div>
- </Transition>
- <BaseButton type="submit" size="lg" class="mt-2" :loading="loading" :disabled="!canSubmit || loading">
- {{ loading ? "Entrando..." : "Entrar" }}
- </BaseButton>
- </form>
- </div>
- </main>
- <ModalEsqueciSenha v-if="modalEsqueciSenha" @close="modalEsqueciSenha = false" />
- </div>
- </template>
- <style scoped>
- .error-banner-enter-active,
- .error-banner-leave-active {
- transition: none;
- }
- @media (prefers-reduced-motion: no-preference) {
- .error-banner-enter-active,
- .error-banner-leave-active {
- transition: opacity 0.18s ease, transform 0.18s ease;
- }
- .error-banner-enter-from,
- .error-banner-leave-to {
- opacity: 0;
- transform: translateY(-4px);
- }
- }
- </style>
|