📚 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:
- A SPA foi estruturada com Vite, TypeScript e Vue Router no Módulo 06.
- A dependência
axiosestá instalada no diretóriotecloja-frontend. - O arquivo CSS global de estilos premium está devidamente importado no arquivo de montagem raiz
src/main.ts.
🤔 Por que fizemos assim?
- Por que usar Composables (como
useAuth) para gerenciar o estado em vez do Pinia/Vuex? O Vue 3 introduziu a Composition API com reatividade nativa (ref,computed). Declarar um estado reativo global no nível do módulo (fora da funçãouseAuth) garante que ele aja como um padrão Singleton (Single Source of Truth) compartilhado entre todos os componentes que importarem a função. Para fluxos básicos de autenticação e sessão, essa abordagem dispensa a necessidade de configurar bibliotecas de gerência de estado complexas como Pinia, mantendo o frontend enxuto e didático. - Por que usar Request Interceptors do Axios? Anexar manualmente o token JWT em cada chamada HTTP (
axios.get('/produtos', { headers: ... })) espalha código repetitivo e aumenta as chances de esquecer a segurança em novos endpoints. O interceptador atua como um middleware centralizado: ele intercepta qualquer requisição HTTP feita pela instância customizada (api) e acopla dinamicamente o cabeçalhoAuthorization: Bearer <token>de forma transparente para o desenvolvedor. - Por que usar Navigation Guards (
beforeEach) no Vue Router? Impede que um usuário comum acesse telas administrativas simplesmente digitando o endereço/adminna URL do navegador. Embora o backend seja a barreira de segurança definitiva (rejeitando as requisições à API sem token válido), o guard do lado do cliente impede a renderização de telas confidenciais que não pertencem ao nível de privilégio do usuário logado, otimizando a experiência do usuário (UX).
🔍 Checkpoint
- Interceptação Operacional: Verifique se as requisições feitas pela instância
apicontêm o cabeçalhoAuthorizationcom a stringBearer ...nas ferramentas de desenvolvedor do navegador (Aba Rede/Network). - 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. - Visual Premium Carregado: Confirme se o arquivo
src/assets/styles.cssfoi importado corretamente emsrc/main.tse 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!