📚 Módulo 07: Frontend - Autenticação Reativa, Interceptor e Estilos

Neste módulo prático do frontend, programaremos a segurança lógica do nosso cliente SPA em Vue 3. Criaremos um Composable reativo (useAuth) para gerenciar sessões e tokens JWT, configuraremos Interceptores HTTP no Axios para autenticação transparente, blindaremos rotas com Navigation Guards e definiremos a folha de estilos premium baseada em Glassmorphism usando CSS moderno.


🔐 1. Composable de Autenticação Reativo (useAuth)

No Vue 3, o padrão Composable é o substituto moderno de mixins e do Vuex. Ele encapsula o estado e a reatividade de forma limpa, semelhante a um serviço do Angular baseado em Signals.

Crie o arquivo em src/composables/useAuth.ts:

import { ref, computed } from 'vue';
import axios from 'axios';
import { UsuarioLogado } from '../models';

const API_URL = 'https://tecloja-api.render.com/api/auth';

// Estado persistido na memória global do módulo (Single Source of Truth)
const usuario = ref<UsuarioLogado | null>(null);

// Inicializa a sessão lendo do localStorage do navegador
const tokenSalvo = localStorage.getItem('tecloja_user');
if (tokenSalvo) {
  usuario.value = JSON.parse(tokenSalvo);
}

export function useAuth() {
  
  // Getters computados reativos
  const currentUser = computed(() => usuario.value);
  const isAuthenticated = computed(() => !!usuario.value);
  const isAdmin = computed(() => usuario.value?.papel === 'ROLE_ADMIN');

  const login = async (username: string, password: str) => {
    try {
      const response = await axios.post(`${API_URL}/login`, { username, password });
      const dados: UsuarioLogado = response.data;
      
      usuario.value = dados;
      localStorage.setItem('tecloja_user', JSON.stringify(dados));
    } catch (err: any) {
      throw new Error(err.response?.data?.detail || "Erro ao realizar autenticação.");
    }
  };

  const logout = () => {
    usuario.value = null;
    localStorage.removeItem('tecloja_user');
  };

  return {
    currentUser,
    isAuthenticated,
    isAdmin,
    login,
    logout
  };
}

🚏 2. Interceptador de Requisições HTTP (Axios API Instance)

Em Engenharia de Software, anexar manualmente o token JWT em cada chamada Ajax é um antipadrão ineficiente. A boa prática é centralizar as chamadas HTTP no Axios e programar um Request Interceptor que injeta de forma transparente o cabeçalho Authorization: Bearer <token> em todas as chamadas.

flowchart LR
    A[Componente Vue] -->|api.get('/produtos')| B{Axios Interceptor}
    B --> C{Token no useAuth?}
    C -- Sim --> D[Injeta Header: Authorization]
    C -- Não --> E[Mantém config original]
    D --> F[FastAPI Backend]
    E --> F
    
    style B fill:#f9f,stroke:#333,stroke-width:2px

Crie o arquivo em src/services/api.ts:

import axios from 'axios';
import { useAuth } from '../composables/useAuth';

const api = axios.create({
  baseURL: 'https://tecloja-api.render.com/api'
});

// Interceptador de Requisições: Anexa o Bearer JWT automaticamente
api.interceptors.request.use(
  (config) => {
    const { currentUser } = useAuth();
    if (currentUser.value?.token) {
      config.headers.Authorization = `Bearer ${currentUser.value.token}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

export default api;

🛡️ 3. Navigation Guards (Segurança de Rotas no Vue)

Para impedir que usuários maliciosos digitem a URL /admin/produtos no navegador e acessem o painel administrativo, acoplamos um Navigation Guard no roteador virtual, verificando se o usuário possui o papel ROLE_ADMIN antes de permitir a transição.

Abra e edite o arquivo src/router/index.ts adicionando a verificação no final:

import router from './index'; // Importa a instância ativa
import { useAuth } from '../composables/useAuth';

router.beforeEach((to, from, next) => {
  const { isAuthenticated, isAdmin } = useAuth();

  // Verifica se o destino começa com o caminho administrativo
  if (to.path.startsWith('/admin')) {
    if (!isAuthenticated.value) {
      // Redireciona para o login se não autenticado
      next({ name: 'Login' });
    } else if (!isAdmin.value) {
      // Barra o acesso se logado mas sem permissão de admin
      alert("Acesso restrito. Somente administradores!");
      next({ name: 'Catalogo' });
    } else {
      next(); // Permite transição livre
    }
  } else {
    next(); // Transição livre
  }
});

🎨 4. Design System Premium: Glassmorphism (styles.css)

Forneceremos um visual moderno de altíssimo nível, utilizando HSL para cores harmônicas, variáveis CSS e efeitos translúcidos em Glassmorphism que geram uma forte primeira impressão aos alunos.

Crie o arquivo em src/assets/styles.css:

/* Importação de fonte moderna via Google Fonts */
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap');

:root {
  --bg-gradient: linear-gradient(135deg, #0f172a 0%, #1e1b4b 100%);
  --glass-bg: rgba(255, 255, 255, 0.03);
  --glass-border: rgba(255, 255, 255, 0.08);
  --text-primary: #f8fafc;
  --text-secondary: #94a3b8;
  --accent-color: #38bdf8; /* Azul neon premium */
  --success-color: #4ade80;
  --danger-color: #f87171;
  --border-color: rgba(255, 255, 255, 0.1);
  --font-family: 'Outfit', sans-serif;
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  background: var(--bg-gradient);
  color: var(--text-primary);
  font-family: var(--font-family);
  min-height: 100vh;
  overflow-x: hidden;
}

/* Glassmorphism Panel */
.glass-panel {
  background: var(--glass-bg);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border: 1px solid var(--glass-border);
  border-radius: 16px;
  padding: 2rem;
  box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3);
}

/* Formulários */
.form-group {
  margin-bottom: 1.5rem;
}

.form-label {
  display: block;
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--text-secondary);
  margin-bottom: 0.5rem;
}

.form-control {
  width: 100%;
  padding: 0.75rem 1rem;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--glass-border);
  border-radius: 8px;
  color: var(--text-primary);
  font-family: var(--font-family);
  transition: all 0.3s ease;
}

.form-control:focus {
  outline: none;
  border-color: var(--accent-color);
  background: rgba(255, 255, 255, 0.08);
  box-shadow: 0 0 10px rgba(56, 189, 248, 0.25);
}

/* Botões */
.btn {
  display: inline-flex;
  align-items: center;
  padding: 0.75rem 1.5rem;
  font-weight: 600;
  border-radius: 8px;
  border: none;
  cursor: pointer;
  font-family: var(--font-family);
  transition: all 0.2s ease;
}

.btn-primary {
  background: var(--accent-color);
  color: #0f172a;
}

.btn-primary:hover:not(:disabled) {
  transform: translateY(-2px);
  box-shadow: 0 0 15px rgba(56, 189, 248, 0.4);
}

.btn-danger {
  background: var(--danger-color);
  color: #0f172a;
}

.btn-danger:hover {
  transform: translateY(-2px);
  box-shadow: 0 0 15px rgba(248, 113, 113, 0.4);
}

.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Layout Geral da Navbar */
.navbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1.2rem 5%;
  background: rgba(15, 23, 42, 0.8);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--glass-border);
}

.navbar-brand {
  font-size: 1.5rem;
  font-weight: 700;
  color: var(--text-primary);
  text-decoration: none;
}

.navbar-menu {
  display: flex;
  list-style: none;
  align-items: center;
  gap: 1.5rem;
}

.navbar-link {
  color: var(--text-secondary);
  text-decoration: none;
  font-weight: 600;
  transition: color 0.2s;
}

.navbar-link:hover, .navbar-link.active {
  color: var(--accent-color);
}


✅ Pré-Requisitos deste Módulo

Antes de passar para a criação das telas interativas e controle do carrinho, certifique-se de que:


🤔 Por que fizemos assim?


🔍 Checkpoint

  1. Interceptação Operacional: Verifique se as requisições feitas pela instância api contêm o cabeçalho Authorization com a string Bearer ... nas ferramentas de desenvolvedor do navegador (Aba Rede/Network).
  2. Proteção de Rotas: Sem estar logado, tente forçar a URL do navegador diretamente para http://localhost:5173/admin/produtos. A aplicação deve barrar a renderização e redirecionar você de forma instantânea para a rota /login.
  3. Visual Premium Carregado: Confirme se o arquivo src/assets/styles.css foi importado corretamente em src/main.ts e se os fundos da tela adotam o gradiente escuro e tipografia limpa.

⚠️ Erros Comuns

Erro Causa Solução
Chamadas privadas retornando 401 Unauthorized mesmo com usuário logado O componente importou a biblioteca padrão do axios (import axios from 'axios') em vez da instância configurada com interceptores. Substitua todas as importações brutas do axios nas views pela nossa instância personalizada: import api from '../services/api'.
Loop infinito de redirecionamentos no navegador O Navigation Guard redireciona para a rota /login sem verificar se o destino atual já é a própria tela de login. Certifique-se de que a rota /login (ou rotas públicas) não entra na lógica de validação do prefixo /admin e sempre finalize o interceptador chamando next() para dar vazão à navegação.
O efeito de desfoque de fundo (blur) do Glassmorphism não renderiza O navegador utilizado é legado e não suporta o filtro nativo ou o elemento pai possui alguma propriedade que remove o contexto de empilhamento de renderização. Adicione sempre o prefixo -webkit-backdrop-filter: blur(...) junto do backdrop-filter: blur(...) clássico nas regras de CSS para garantir compatibilidade cruzada.

🏁 Conclusão

Sensacional! Toda a base de segurança, consumo de API assíncrono com injeção automática de Bearer JWT, bloqueio físico de navegação não permitida e folha de estilos premium estão completamente consolidados.

No Módulo 08, criaremos os componentes visuais interativos. Programaremos o catálogo de eletrônicos, o composable reativo do Carrinho de Compras utilizando propriedades computadas do Vue 3 e construiremos o formulário de cadastro/edição administrativa do catálogo!


Voltar para o Sumário