Sfoglia il codice sorgente

adicionado pagina de login

leonardo 3 mesi fa
parent
commit
8b3cc0d6ef

+ 2 - 2
dist/index.html

@@ -6,8 +6,8 @@
     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
     <link rel="icon" type="image/x-icon" href="/favicon.ico?v=2" />
     <title>ORÁCULO</title>
-    <script type="module" crossorigin src="/assets/index-DzMOoX-5.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-WiIdW28C.css">
+    <script type="module" crossorigin src="/assets/index-DRIqSdBF.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-B1uWbpcA.css">
   </head>
   <body class="bg-background">
     <noscript>

+ 57 - 0
src/api/auth.js

@@ -0,0 +1,57 @@
+import { apiBaseUrl } from "./client.js";
+
+async function parseResponse(res) {
+  const contentType = res.headers.get("content-type") ?? "";
+  if (contentType.includes("application/json")) {
+    return res.json().catch(() => ({}));
+  }
+
+  const text = await res.text().catch(() => "");
+  return text ? { message: text } : {};
+}
+
+function buildErrorMessage(payload, status) {
+  if (payload?.msg) return payload.msg;
+  if (payload?.error) return payload.error;
+  if (payload?.message) return payload.message;
+  return `http_error:${status}`;
+}
+
+export async function loginRequest({ login, senha, rememberMe = false }) {
+  const res = await fetch(`${apiBaseUrl}/api/auth/login`, {
+    method: "POST",
+    headers: {
+      "content-type": "application/json"
+    },
+    body: JSON.stringify({
+      login,
+      senha,
+      RemenberMe: rememberMe
+    })
+  });
+
+  const payload = await parseResponse(res);
+
+  if (!res.ok || payload?.status === false) {
+    throw new Error(buildErrorMessage(payload, res.status));
+  }
+
+  return payload;
+}
+
+export async function logoutRequest() {
+  const res = await fetch(`${apiBaseUrl}/api/auth/logout`, {
+    method: "POST",
+    headers: {
+      "content-type": "application/json"
+    }
+  });
+
+  const payload = await parseResponse(res);
+
+  if (!res.ok || payload?.status === false) {
+    throw new Error(buildErrorMessage(payload, res.status));
+  }
+
+  return payload;
+}

+ 80 - 0
src/composables/useAuth.js

@@ -0,0 +1,80 @@
+import { computed, readonly, ref } from "vue";
+import { loginRequest, logoutRequest } from "../api/auth.js";
+
+const LOCAL_STORAGE_KEY = "auth.session";
+const SESSION_STORAGE_KEY = "auth.session.temp";
+
+function getStorage(kind) {
+  if (typeof window === "undefined") return null;
+  return kind === "local" ? window.localStorage : window.sessionStorage;
+}
+
+function loadStoredSession() {
+  for (const kind of ["local", "session"]) {
+    try {
+      const storage = getStorage(kind);
+      const raw = storage?.getItem(kind === "local" ? LOCAL_STORAGE_KEY : SESSION_STORAGE_KEY);
+      if (!raw) continue;
+      const parsed = JSON.parse(raw);
+      if (parsed?.usuario) return parsed;
+    } catch {}
+  }
+
+  return null;
+}
+
+function persistSession(session) {
+  try {
+    getStorage("local")?.removeItem(LOCAL_STORAGE_KEY);
+    getStorage("session")?.removeItem(SESSION_STORAGE_KEY);
+
+    if (session?.rememberMe) {
+      getStorage("local")?.setItem(LOCAL_STORAGE_KEY, JSON.stringify(session));
+      return;
+    }
+
+    if (session) {
+      getStorage("session")?.setItem(SESSION_STORAGE_KEY, JSON.stringify(session));
+    }
+  } catch {}
+}
+
+const session = ref(loadStoredSession());
+const user = computed(() => session.value?.usuario ?? null);
+const isAuthenticated = computed(() => Boolean(user.value?.Id));
+
+async function login({ login, senha, rememberMe = false }) {
+  const payload = await loginRequest({ login, senha, rememberMe });
+  session.value = {
+    usuario: payload.usuario,
+    rememberMe,
+    savedAt: new Date().toISOString()
+  };
+  persistSession(session.value);
+  return payload;
+}
+
+async function logout() {
+  try {
+    await logoutRequest();
+  } finally {
+    session.value = null;
+    persistSession(null);
+  }
+}
+
+function clearSession() {
+  session.value = null;
+  persistSession(null);
+}
+
+export function useAuth() {
+  return {
+    session: readonly(session),
+    user,
+    isAuthenticated,
+    login,
+    logout,
+    clearSession
+  };
+}

+ 35 - 1
src/layout/LayoutSistema.vue

@@ -15,10 +15,12 @@ import SearchBox from "../components/SearchBox.vue";
 import { ingestDocuments, ingestFile, listDocuments } from "../api/chat.js";
 import { useChat } from "../composables/useChat.js";
 import { useSearch } from "../composables/useSearch.js";
+import { useAuth } from "../composables/useAuth.js";
 
 const router = useRouter();
 const route = useRoute();
 const { newConversation } = useChat();
+const { user, logout } = useAuth();
 
 const {
   results: searchResults,
@@ -32,10 +34,13 @@ const docs = reactive({
   loading: false,
   error: "",
 });
+const logoutLoading = ref(false);
 
 const sidebarRecolhida = ref(false);
 const painelAberto = ref("");
 const rotaAtiva = computed(() => String(route.name ?? ""));
+const usuarioNome = computed(() => user.value?.Nome || user.value?.Login || "Usuario");
+const usuarioSetor = computed(() => user.value?.Setor || user.value?.Nivel || "");
 const layoutStyle = computed(() => ({
   "--layout-sidebar-width": sidebarRecolhida.value ? "84px" : "280px",
 }));
@@ -96,6 +101,19 @@ async function novaConversa() {
   await router.push({ name: "home" });
 }
 
+async function encerrarSessao() {
+  if (logoutLoading.value) return;
+
+  logoutLoading.value = true;
+
+  try {
+    await logout();
+    await router.replace({ name: "login" });
+  } finally {
+    logoutLoading.value = false;
+  }
+}
+
 async function loadDocs() {
   docs.loading = true;
   docs.error = "";
@@ -205,7 +223,23 @@ watch(
           <div class="min-w-0">
             <div class="truncate text-base font-medium">{{ tituloPagina }}</div>
           </div>
-          <BotaoAlterarTema />
+          <div class="flex items-center gap-3">
+            <div class="hidden text-right md:block">
+              <div class="text-sm font-semibold leading-tight">
+                {{ usuarioNome }}
+              </div>
+             
+            </div>
+            <BotaoAlterarTema />
+            <button
+              type="button"
+              class="rounded-lg !border-primary !bg-none !bg-primary px-3 py-2 text-sm font-medium !text-primary-foreground !transform-none transition-colors hover:!transform-none hover:!border-[var(--primary-600)] hover:!bg-[var(--primary-600)]"
+              :disabled="logoutLoading"
+              @click="encerrarSessao"
+            >
+              {{ logoutLoading ? "Saindo..." : "Sair" }}
+            </button>
+          </div>
         </div>
       </header>
 

+ 23 - 3
src/router/index.js

@@ -2,13 +2,33 @@ import { createRouter, createWebHistory } from "vue-router";
 import PaginaInicialView from "../views/pagina-inicial/PaginaInicialView.vue";
 import ConversasHistoricoView from "../views/conversas/ConversasHistoricoView.vue";
 import ConversasPesquisarView from "../views/conversas/ConversasPesquisarView.vue";
+import LoginView from "../views/auth/LoginView.vue";
+import { useAuth } from "../composables/useAuth.js";
 
 export const router = createRouter({
   history: createWebHistory(import.meta.env.BASE_URL),
   routes: [
-    { path: "/", name: "home", component: PaginaInicialView },
-    { path: "/conversas/historico", name: "conversas-historico", component: ConversasHistoricoView },
-    { path: "/conversas/pesquisar", name: "conversas-pesquisar", component: ConversasPesquisarView },
+    { path: "/login", name: "login", component: LoginView, meta: { guestOnly: true } },
+    { path: "/", name: "home", component: PaginaInicialView, meta: { requiresAuth: true } },
+    { path: "/conversas/historico", name: "conversas-historico", component: ConversasHistoricoView, meta: { requiresAuth: true } },
+    { path: "/conversas/pesquisar", name: "conversas-pesquisar", component: ConversasPesquisarView, meta: { requiresAuth: true } },
     { path: "/:pathMatch(.*)*", redirect: "/" }
   ]
 });
+
+router.beforeEach((to) => {
+  const { isAuthenticated } = useAuth();
+
+  if (to.meta?.guestOnly && isAuthenticated.value) {
+    return { name: "home" };
+  }
+
+  if (to.meta?.requiresAuth && !isAuthenticated.value) {
+    return {
+      name: "login",
+      query: to.fullPath && to.fullPath !== "/" ? { redirect: to.fullPath } : {}
+    };
+  }
+
+  return true;
+});

+ 120 - 0
src/views/auth/LoginView.vue

@@ -0,0 +1,120 @@
+<script setup>
+import { computed, ref } from "vue";
+import { useRoute, useRouter } from "vue-router";
+import BotaoAlterarTema from "../../components/BotaoAlterarTema.vue";
+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 error = ref("");
+
+const canSubmit = computed(() => {
+  return Boolean(form.value.login.trim() && form.value.senha);
+});
+
+async function onSubmit() {
+  if (!canSubmit.value || loading.value) return;
+
+  loading.value = true;
+  error.value = "";
+
+  try {
+    await login(form.value);
+    const redirect = typeof route.query.redirect === "string" ? route.query.redirect : "/";
+    await router.replace(redirect);
+  } catch (err) {
+    error.value = err?.message || "Falha ao realizar login.";
+  } finally {
+    loading.value = false;
+  }
+}
+</script>
+
+<template>
+  <div class="relative min-h-screen overflow-hidden bg-background text-foreground">
+    <div
+      aria-hidden="true"
+      class="pointer-events-none absolute inset-0 z-0 bg-[radial-gradient(900px_520px_at_20%_10%,rgba(37,99,235,0.12),transparent_60%),radial-gradient(700px_520px_at_85%_35%,rgba(162,102,255,0.10),transparent_60%)] dark:hidden"
+    />
+    <div
+      aria-hidden="true"
+      class="pointer-events-none absolute inset-0 z-0 hidden bg-[radial-gradient(900px_520px_at_20%_10%,rgba(91,140,255,0.18),transparent_60%),radial-gradient(700px_520px_at_85%_35%,rgba(162,102,255,0.14),transparent_60%)] dark:block"
+    />
+
+    <header class="relative z-10 flex items-center justify-between px-6 py-5">
+      <div>
+        <div class="text-[16px] font-extrabold tracking-[0.14em]">ORACULO</div>
+        <div class="mt-1 text-sm text-gray-500 dark:text-gray-400">
+          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-200 bg-white/80 p-6 shadow-sm backdrop-blur-md dark:border-white/10 dark:bg-gradient-to-b dark:from-[#101827] dark:to-[#070b16] dark:shadow-[0_18px_50px_rgba(0,0,0,0.35)]"
+      >
+        <div class="text-2xl font-extrabold tracking-tight">Entrar</div>
+        <div class="mt-2 text-sm text-gray-500 dark:text-gray-400">
+          Informe seu usuario ou email e sua senha para acessar o sistema.
+        </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
+              v-model="form.login"
+              type="text"
+              autocomplete="username"
+              placeholder="Digite seu login"
+            />
+          </label>
+
+          <label class="grid gap-1.5">
+            <span class="text-sm font-medium">Senha</span>
+            <input
+              v-model="form.senha"
+              type="password"
+              autocomplete="current-password"
+              placeholder="Digite sua senha"
+            />
+          </label>
+
+          <label class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300">
+            <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>
+
+          <div
+            v-if="error"
+            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"
+          >
+            {{ error }}
+          </div>
+
+          <button
+            type="submit"
+            class="mt-2 inline-flex items-center justify-center rounded-xl border border-primary bg-primary px-4 py-2.5 text-sm font-semibold text-primary-foreground transition-colors hover:border-[var(--primary-600)] hover:bg-[var(--primary-600)]"
+            :disabled="!canSubmit || loading"
+          >
+            {{ loading ? "Entrando..." : "Entrar" }}
+          </button>
+        </form>
+      </div>
+    </main>
+  </div>
+</template>