Sistema de Autenticação
Implementação completa de autenticação com email/senha, CSRF tokens, cookies seguros e integração com a API do Restaurantix.
🎯 Por que Cookies ao invés de JWT no localStorage?
- • Segurança XSS: Cookies HttpOnly não podem ser acessados via JavaScript
- • Automático: Browser envia cookies automaticamente em cada requisição
- • CSRF Protection: Usamos tokens CSRF para validação adicional
- • Server-side: Controle total do servidor sobre autenticação
O Restaurantix implementa autenticação tradicional com email/senha, usando cookies seguros para JWT tokens e proteção CSRF. Esta abordagem é mais segura que armazenar tokens no localStorage, especialmente contra ataques XSS.
📋 Fluxo de Autenticação Completo
Cliente solicita token CSRF do servidor antes de qualquer operação sensível
Usuário envia email/senha + CSRF token no header da requisição
Servidor valida credenciais e define cookie httpOnly com JWT
Browser envia cookie automaticamente em requisições subsequentes
Middleware do servidor valida JWT e autoriza acesso
🛡️ Camadas de Segurança
Cookies inacessíveis via JavaScript, protege contra XSS
Tokens únicos em headers previnem ataques Cross-Site
Política que bloqueia cookies de sites externos
Cookies só enviados via HTTPS em produção
Tokens têm tempo limitado de vida
⚠️ Conceitos Importantes para Entender
CSRF (Cross-Site Request Forgery):
Ataque onde um site malicioso força seu browser a fazer requisições em outro site onde você está logado.
XSS (Cross-Site Scripting):
Ataque onde código JavaScript malicioso é injetado e executado no seu browser.
HttpOnly Cookie:
Cookie que não pode ser acessado via JavaScript, apenas pelo servidor.
SameSite Policy:
Política que controla quando cookies são enviados em requisições cross-site.
🎯 Objetivo desta Seção
Vamos criar uma estrutura robusta para comunicação com a API do backend, implementando: validação de variáveis de ambiente, route handlers no Next.js e cliente HTTP com proteção CSRF.
📁 src/lib/env.ts - Validação Segura de Variáveis
💡 Por que validar variáveis de ambiente?
- • Falha rápida: Aplicação para se variáveis estão ausentes
- • TypeScript safety: Garantia de tipos nas variáveis
- • Documentação: Código autodocumentado sobre dependências
- • Debugging: Erros claros quando configuração está incorreta
// 📁 Validação de variáveis de ambiente com Zod
import { z } from 'zod';
// 🔧 Schema de validação - garante que todas as env vars estão presentes e válidas
const envSchema = z.object({
NEXT_PUBLIC_API_URL: z.string().url(), // ✅ Valida se é uma URL válida
NEXT_PUBLIC_APP_NAME: z.string().min(1), // ✅ Não pode ser string vazia
NEXT_PUBLIC_APP_VERSION: z.string().min(1), // ✅ Versão obrigatória
});
// 🚀 Parse das variáveis - falha na inicialização se algo estiver errado
export const env = envSchema.parse({
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
NEXT_PUBLIC_APP_NAME: process.env.NEXT_PUBLIC_APP_NAME,
NEXT_PUBLIC_APP_VERSION: process.env.NEXT_PUBLIC_APP_VERSION,
});
// 💡 Uso seguro em qualquer lugar da aplicação:
// import { env } from '@/lib/env'
// const apiUrl = env.NEXT_PUBLIC_API_URL // TypeScript sabe que é string válida
// 🔍 Exemplo de arquivo .env.local:
// NEXT_PUBLIC_API_URL=http://localhost:8080
// NEXT_PUBLIC_APP_NAME=Restaurantix
// NEXT_PUBLIC_APP_VERSION=1.0.0🛡️ Route Handlers - Proxy Seguro para o Backend
🤔 Por que usar Route Handlers como proxy?
- • CORS simplificado: Evita problemas de Cross-Origin requests
- • Cookies seguros: Servidor pode definir HttpOnly cookies
- • Middleware centralizado: Validação e logs em um lugar
- • Abstração: Frontend não precisa saber detalhes do backend
🔐 CSRF Token Route Handler
import { NextResponse } from 'next/server';
import { env } from '@/lib/env';
export async function GET() {
try {
// 🔗 Fazemos proxy da requisição para o backend
const response = await fetch(`${env.NEXT_PUBLIC_API_URL}/csrf-token`, {
method: 'GET',
headers: {
Accept: 'application/json',
},
credentials: 'include', // ✅ Importante: inclui cookies na requisição
});
const data = await response.json();
if (!response.ok) {
return NextResponse.json(
{ error: data.message || 'Erro ao obter CSRF token' },
{ status: response.status },
);
}
// 📦 Criamos a resposta para o cliente
const nextResponse = NextResponse.json(data);
// 🍪 CRUCIAL: Propagamos os cookies do backend para o cliente
const setCookieHeader = response.headers.get('set-cookie');
if (setCookieHeader) {
// O Next.js espera múltiplos Set-Cookie headers como um array
const cookies = setCookieHeader.split(', ');
cookies.forEach((cookie) => {
nextResponse.headers.append('Set-Cookie', cookie);
});
}
return nextResponse;
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.error('🚨 Erro no route handler de csrf-token:', error);
}
return NextResponse.json({ error: 'Erro ao processar requisição' }, { status: 500 });
}
}
// 🎯 Este route handler será acessível em /api/csrf-token
// e funcionará como um proxy seguro para o backend👤 Route Handler de Cadastro (Sign-up)
import { NextRequest, NextResponse } from 'next/server';
import { env } from '@/lib/env';
export async function POST(request: NextRequest) {
try {
// 📥 Pegamos os dados do corpo da requisição
const body = await request.json();
// 🔗 Fazemos proxy para o endpoint de cadastro do backend
const response = await fetch(`${env.NEXT_PUBLIC_API_URL}/auth/sign-up`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify(body), // 📤 Repassamos os dados recebidos
credentials: 'include', // ✅ Inclui cookies (importante para CSRF)
});
const data = await response.json();
if (!response.ok) {
return NextResponse.json(
{ error: data.message || 'Erro ao criar conta' },
{ status: response.status },
);
}
// 📦 Criamos resposta de sucesso
const nextResponse = NextResponse.json(data);
// 🍪 CRUCIAL: Propagamos cookies de autenticação do backend
const setCookieHeader = response.headers.get('set-cookie');
if (setCookieHeader) {
// Cookies podem conter: auth token, csrf token, session info
const cookies = setCookieHeader.split(', ');
cookies.forEach((cookie) => {
nextResponse.headers.append('Set-Cookie', cookie);
});
}
return nextResponse;
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.error('🚨 Erro no route handler de sign-up:', error);
}
return NextResponse.json({ error: 'Erro ao processar requisição' }, { status: 500 });
}
}
// 📝 Dados esperados no body:
// {
// "name": "João Silva",
// "email": "joao@email.com",
// "password": "senha123",
// "role": "customer" | "manager"
// }🔑 Route Handler de Login (Sign-in)
import { NextRequest, NextResponse } from 'next/server';
import { env } from '@/lib/env';
export async function POST(request: NextRequest) {
try {
const body = await request.json();
// 🔗 Fazemos proxy para o endpoint de login do backend
const response = await fetch(`${env.NEXT_PUBLIC_API_URL}/auth/sign-in`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify(body),
credentials: 'include', // ✅ Para enviar CSRF token via cookies
});
const data = await response.json();
if (!response.ok) {
return NextResponse.json(
{ error: data.message || 'Erro ao fazer login' },
{ status: response.status },
);
}
// 📦 Resposta de sucesso
const nextResponse = NextResponse.json(data);
// 🍪 Propagamos o cookie de autenticação JWT
const setCookieHeader = response.headers.get('set-cookie');
if (setCookieHeader) {
const cookies = setCookieHeader.split(', ');
cookies.forEach((cookie) => {
nextResponse.headers.append('Set-Cookie', cookie);
});
}
return nextResponse;
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.error('🚨 Erro no route handler de sign-in:', error);
}
return NextResponse.json({ error: 'Erro ao processar requisição' }, { status: 500 });
}
}
// 📝 Dados esperados no body:
// {
// "email": "joao@email.com",
// "password": "senha123"
// }👤 Route Handler de Perfil (/me)
import { NextRequest, NextResponse } from 'next/server';
import { env } from '@/lib/env';
export async function GET(request: NextRequest) {
try {
// 🍪 Pegamos os cookies da requisição atual (incluindo JWT)
const cookieHeader = request.headers.get('cookie') || '';
// 🔗 Fazemos requisição para o backend incluindo os cookies
const response = await fetch(`${env.NEXT_PUBLIC_API_URL}/me`, {
method: 'GET',
headers: {
Cookie: cookieHeader, // 📤 Repassamos todos os cookies
Accept: 'application/json',
},
});
if (!response.ok) {
return NextResponse.json(
{ error: 'Usuário não autenticado' },
{ status: response.status }
);
}
const data = await response.json();
// 📦 Resposta com dados do usuário
const nextResponse = NextResponse.json(data);
// 🍪 Propagamos novos cookies se houver (refresh de tokens, etc.)
const setCookieHeaders = response.headers.get('set-cookie');
if (setCookieHeaders) {
const cookies = setCookieHeaders.split(', ');
cookies.forEach((cookie) => {
nextResponse.headers.append('Set-Cookie', cookie);
});
}
return nextResponse;
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.error('🚨 Erro no route handler de perfil:', error);
}
return NextResponse.json({ error: 'Erro ao processar requisição' }, { status: 500 });
}
}
// 📋 Resposta esperada:
// {
// "id": "user-123",
// "name": "João Silva",
// "email": "joao@email.com",
// "role": "customer"
// }🚀 src/lib/api.ts - Cliente HTTP Inteligente
🧠 O que torna este cliente "inteligente"?
- • CSRF automático: Obtém e inclui tokens CSRF quando necessário
- • Retry logic: Tenta novamente com novo token se CSRF expirar
- • Roteamento inteligente: Usa route handlers para auth, API direta para dados
- • Error handling: Tratamento centralizado de erros
/* eslint-disable no-console */
import { env } from './env';
class ApiClient {
private baseURL: string;
private csrfToken: string | null = null; // 🔐 Cache do token CSRF
constructor() {
this.baseURL = env.NEXT_PUBLIC_API_URL;
}
// ✅ Buscar token CSRF do servidor via Route Handler local
async getCsrfToken(): Promise<string> {
try {
console.log('🔐 Tentando obter token CSRF via Route Handler local');
// 🎯 Usamos nosso route handler como proxy
const response = await fetch('/api/csrf-token', {
method: 'GET',
credentials: 'include', // ✅ Essencial para cookies
headers: {
Accept: 'application/json',
'Cache-Control': 'no-cache', // 🚫 Sempre buscar token fresco
Pragma: 'no-cache',
},
mode: 'cors',
cache: 'no-store', // 🚫 Não cachear resposta
});
if (!response.ok) {
const errorText = await response.text();
console.error('🔐 Erro na resposta CSRF:', errorText);
throw new Error(`Erro HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (!data.token) {
throw new Error('Token CSRF não recebido do servidor');
}
this.csrfToken = data.token; // 💾 Salvamos no cache
return data.token;
} catch (error) {
console.error('🔐 Erro ao obter token CSRF:', error);
throw new Error('Erro ao obter token CSRF');
}
}
// ✅ Método privado para requisições com lógica inteligente
private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
// 🎯 Roteamento inteligente: auth via route handlers, dados via API direta
const authRoutes = ['/auth/sign-in', '/auth/sign-up', '/csrf-token', '/me'];
const isAuthRoute = authRoutes.some((route) => endpoint.includes(route));
// 🔀 Decisão de rota: local para auth, backend direto para dados
const url = isAuthRoute ? `/api${endpoint}` : `${this.baseURL}${endpoint}`;
// 🛡️ Verificação de métodos que precisam de CSRF
const unsafeMethods = ['POST', 'PUT', 'PATCH', 'DELETE'];
const method = (options.method || 'GET').toUpperCase();
// ✅ Para métodos não seguros em rotas não-auth, garantir CSRF token
if (unsafeMethods.includes(method) && !isAuthRoute) {
const isLoginEndpoint = endpoint === '/auth/sign-in';
if (!isLoginEndpoint || this.csrfToken) {
try {
await this.getCsrfToken();
} catch (error) {
console.warn('⚠️ Falha ao obter token CSRF:', error);
// Para login inicial, continuar sem CSRF
}
}
}
// 🔧 Configuração da requisição
const config: RequestInit = {
credentials: 'include', // ✅ Sempre incluir cookies
mode: 'cors', // ✅ Permitir CORS para desenvolvimento
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
// ✅ Headers para compatibilidade com CSRF strict
'Cache-Control': 'no-cache',
Pragma: 'no-cache',
// ✅ Incluir CSRF token para métodos não seguros
...(this.csrfToken &&
unsafeMethods.includes(method) &&
!isAuthRoute && {
'X-CSRF-Token': this.csrfToken,
}),
...options.headers,
},
...options,
};
try {
const response = await fetch(url, config);
// ✅ Tratamento inteligente de erros
if (!response.ok) {
let errorMessage = 'Erro na requisição';
try {
const errorData = await response.json();
errorMessage = errorData.message || errorData.error || errorMessage;
} catch {
errorMessage = `Erro HTTP ${response.status}: ${response.statusText}`;
}
// 🔄 Se erro de CSRF, limpar token e tentar novamente (retry logic)
if (response.status === 403 && errorMessage.includes('CSRF')) {
this.csrfToken = null; // 🗑️ Limpar token inválido
console.warn('⚠️ Token CSRF inválido, tentando novamente...');
// 🔄 Tentar uma vez mais com novo token
if (unsafeMethods.includes(method) && !isAuthRoute) {
try {
await this.getCsrfToken();
config.headers = {
...config.headers,
'X-CSRF-Token': this.csrfToken!,
};
const retryResponse = await fetch(url, config);
if (retryResponse.ok) {
return retryResponse.json();
}
} catch {
// Se falhar novamente, deixar o erro original ser lançado
}
}
}
throw new Error(errorMessage);
}
// ✅ Verificar se a resposta tem conteúdo JSON
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return response.json();
}
// Se não for JSON, retornar resposta vazia
return {} as T;
} catch (error) {
console.error(`🚨 Erro na requisição para ${endpoint}:`, error);
throw error;
}
}
// 🎯 Métodos públicos da API (interface limpa para o frontend)
async signUp(data: {
name: string;
email: string;
password: string;
role?: 'manager' | 'customer';
}) {
return this.request('/auth/sign-up', {
method: 'POST',
body: JSON.stringify(data),
});
}
async signIn(email: string, password: string) {
return this.request('/auth/sign-in', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
}
async getProfile() {
return this.request('/me');
}
async signOut() {
return this.request('/sign-out', { method: 'GET' });
}
// 🎯 Métodos para funcionalidades do app
async getOrders(params?: Record<string, string>) {
const searchParams = params ? new URLSearchParams(params) : '';
return this.request(`/orders?${searchParams}`);
}
async getMetrics() {
return this.request('/metrics');
}
// 🔐 Redefinição de senha
async requestPasswordReset(email: string) {
return this.request('/auth/request-password-reset', {
method: 'POST',
body: JSON.stringify({ email }),
});
}
async resetPassword(token: string, newPassword: string) {
return this.request('/auth/reset-password', {
method: 'POST',
body: JSON.stringify({ token, newPassword }),
});
}
}
// 🚀 Exportamos uma instância única (singleton pattern)
export const api = new ApiClient();
// 💡 Uso em componentes:
// import { api } from '@/lib/api'
// const user = await api.getProfile()🔑 Resumo dos Conceitos Implementados
- ✅ Validação de ENV: Falha rápida se configuração incorreta
- ✅ Route Handlers: Proxy seguro evitando CORS
- ✅ Cookie Propagation: Repassa cookies do backend
- ✅ CSRF Protection: Tokens automáticos em requisições
- ✅ Retry Logic: Tenta novamente se CSRF expirar
- ✅ Error Handling: Tratamento centralizado de erros
- ✅ Roteamento Inteligente: Auth local, dados diretos
- ✅ TypeScript Safety: Tipos seguros em toda API
🎯 Por que Zustand para Gerenciamento de Estado?
- • Simplicidade: API mais simples que Redux
- • Sem boilerplate: Menos código redundante
- • TypeScript nativo: Excelente suporte a tipos
- • Persistência fácil: Middleware built-in
- • Performance: Re-renders otimizados
- • DevTools: Integração com React DevTools
- • Flexibilidade: Funciona fora do React
- • Bundle size: Muito menor que Redux
📦 Instalação das Dependências
🔧 Pacotes Necessários:
- • zustand: Biblioteca principal para gerenciamento de estado
- • @types/js-cookie: Tipos TypeScript para manipulação de cookies
- • js-cookie: Utilitários para trabalhar com cookies (opcional para debug)
npm install zustand
npm install @types/js-cookie js-cookie🏪 src/store/auth-store.ts - Store de Autenticação Completo
🧠 Conceitos Implementados no Store:
- • Estado centralizado: Dados do usuário em um local
- • Persistência automática: Mantém estado entre reloads
- • Actions assíncronas: Integração com API
- • Error handling: Tratamento de erros centralizado
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable no-unused-vars */
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { api } from '@/lib/api';
// 📝 Tipagem do usuário - define estrutura dos dados
interface User {
id: string;
name: string;
email: string;
role: 'manager' | 'customer';
}
// 📝 Tipagem dos dados de cadastro
interface SignUpData {
name: string;
email: string;
password: string;
role?: 'manager' | 'customer'; // Opcional, padrão: customer
}
// 🏪 Interface do Store - define todo o estado e ações disponíveis
interface AuthState {
// 📊 Estado (State)
user: User | null; // Dados do usuário logado ou null
isLoading: boolean; // Loading state para UX
isAuthenticated: boolean; // Flag derivada do user
// ⚡ Ações (Actions) - métodos para modificar o estado
signIn: (email: string, password: string) => Promise<void>;
signUp: (data: SignUpData) => Promise<void>;
signOut: () => Promise<void>;
refreshUser: () => Promise<void>;
getProfile: () => Promise<void>;
setLoading: (loading: boolean) => void;
setUser: (user: User | null) => void;
}
// 🏗️ Criação do Store com persist middleware
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
// 🌟 Estado inicial
user: null,
isLoading: true, // Começa como true para verificar auth inicial
isAuthenticated: false,
// 🔧 Ações básicas de estado
setLoading: (loading: boolean) => {
set({ isLoading: loading });
},
setUser: (user: User | null) => {
set({
user,
isAuthenticated: !!user, // Converte user para boolean
});
},
// 🔑 Ação de Login
signIn: async (email: string, password: string) => {
try {
set({ isLoading: true });
// 🌐 Chama API de login
const response = await api.signIn(email, password);
// 🎯 Estratégia: se resposta tem dados do usuário, usar diretamente
if (response && typeof response === 'object' && 'id' in response) {
const user = response as User;
set({
user,
isAuthenticated: true,
isLoading: false,
});
} else {
// 🔄 Fallback: buscar perfil após login bem-sucedido
try {
const userData = (await api.getProfile()) as User;
set({
user: userData,
isAuthenticated: true,
isLoading: false,
});
} catch (profileError) {
// 🚨 Último recurso: usuário temporário
const tempUser = {
id: 'temp',
name: 'Usuário',
email,
role: 'customer' as const
};
set({
user: tempUser,
isAuthenticated: true,
isLoading: false,
});
}
}
} catch (error) {
set({ isLoading: false });
throw new Error('Email ou senha incorretos');
}
},
// 👤 Ação de Cadastro
signUp: async (data: SignUpData) => {
try {
set({ isLoading: true });
// 🌐 Chama API de cadastro
await api.signUp(data);
// 👥 Após cadastro, usuário normalmente já está logado
const userData = (await api.getProfile()) as User;
if (!userData) {
throw new Error('Usuário não encontrado após registro');
}
set({
user: userData,
isAuthenticated: true,
isLoading: false,
});
} catch (error) {
set({ isLoading: false });
if (process.env.NODE_ENV === 'development') {
console.error('🚨 Erro ao criar conta:', error);
}
throw new Error('Erro ao criar conta. Email pode já estar em uso.');
}
},
// 🚪 Ação de Logout
signOut: async () => {
try {
// 🌐 Tenta fazer logout no servidor
await api.signOut();
} catch (error) {
// ⚠️ Ignorar erros de logout - cookies podem já ter expirado
if (process.env.NODE_ENV === 'development') {
console.warn('⚠️ Erro no logout:', error);
}
} finally {
// 🧹 Sempre limpar estado local, independente do resultado
set({
user: null,
isAuthenticated: false,
isLoading: false,
});
}
},
// 🔄 Ação para atualizar dados do usuário
refreshUser: async () => {
try {
set({ isLoading: true });
const userData = (await api.getProfile()) as User;
set({
user: userData,
isAuthenticated: true,
isLoading: false,
});
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.error('🚨 Erro ao atualizar dados do usuário:', error);
}
// 🚨 Se falhar, usuário não está mais autenticado
set({
user: null,
isAuthenticated: false,
isLoading: false,
});
}
},
// 👤 Ação para buscar perfil (similar ao refresh, mas com error handling diferente)
getProfile: async () => {
try {
set({ isLoading: true });
const userData = (await api.getProfile()) as User;
set({
user: userData,
isAuthenticated: true,
isLoading: false,
});
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.error('🚨 Erro ao buscar perfil:', error);
}
set({
user: null,
isAuthenticated: false,
isLoading: false,
});
throw error; // Re-throw para o componente lidar
}
},
}),
{
// ⚙️ Configuração do middleware de persistência
name: 'auth-storage', // Nome da chave no localStorage
partialize: (state) => ({ // Quais partes do estado persistir
user: state.user,
isAuthenticated: state.isAuthenticated,
// ❌ NÃO persistimos isLoading - sempre inicia como true
}),
},
),
);
// 💡 Exemplos de uso nos componentes:
// 📖 Ler estado:
// const { user, isAuthenticated, isLoading } = useAuthStore();
// 🎯 Usar seletores para performance:
// const user = useAuthStore((state) => state.user);
// const signIn = useAuthStore((state) => state.signIn);
// 🏃 Usar shallow para objetos:
// import { useShallow } from 'zustand/react/shallow';
// const { user, isAuthenticated } = useAuthStore(
// useShallow((state) => ({
// user: state.user,
// isAuthenticated: state.isAuthenticated,
// }))
// );🔍 Conceitos Avançados Implementados
Persist Middleware:
Automaticamente salva e carrega estado do localStorage. Útil para manter usuário logado entre sessões.
Partialize:
Escolhe quais partes do estado persistir. Não persistimos isLoading para sempre começar verificando autenticação.
Error Boundaries:
Todas as ações tratam erros graciosamente, mantendo a UI estável mesmo com falhas de rede.
Estado Derivado:
isAuthenticated é derivado de user (!!user), evitando inconsistências de estado.
🎯 Padrões de Uso nos Componentes
📖 Leitura de Estado:
// ✅ Leitura simples
const { user, isAuthenticated, isLoading } = useAuthStore();
// ✅ Seletor específico (melhor performance)
const user = useAuthStore((state) => state.user);
const signIn = useAuthStore((state) => state.signIn);
// ✅ Múltiplos valores com shallow (evita re-renders desnecessários)
import { useShallow } from 'zustand/react/shallow';
const { user, isAuthenticated } = useAuthStore(
useShallow((state) => ({
user: state.user,
isAuthenticated: state.isAuthenticated,
}))
);⚡ Ações Assíncronas:
// ✅ Login com error handling
const signIn = useAuthStore((state) => state.signIn);
const handleLogin = async (email: string, password: string) => {
try {
await signIn(email, password);
router.push('/dashboard');
toast.success('Login realizado com sucesso!');
} catch (error) {
toast.error('Erro ao fazer login');
}
};
// ✅ Logout simples
const signOut = useAuthStore((state) => state.signOut);
const handleLogout = () => {
signOut(); // Sempre funciona, mesmo com erro de rede
router.push('/');
};✅ Vantagens desta Implementação
- 🔒 Seguro: Estado sempre consistente com servidor
- ⚡ Performático: Seletores evitam re-renders desnecessários
- 🔄 Resiliente: Funciona mesmo com falhas de rede
- 💾 Persistente: Mantém login entre sessões
- 🧪 Testável: Lógica isolada e fácil de testar
- 🔧 Manutenível: Código claro e bem documentado
- 📱 Universal: Funciona em web e mobile
- 🎯 TypeSafe: Tipos garantem integridade dos dados
🎯 A Tríade Perfeita: React Hook Form + Zod + Server Actions
Esta combinação oferece uma experiência completa de formulários: performance otimizada, validação robusta e server-side security.
React Hook Form
Performance com uncontrolled inputs e re-renders mínimos
Zod Validation
Schema-first validation com TypeScript safety
Server Actions
Segurança server-side e progressiveenhancement
📁 src/lib/auth-validations.ts - Schemas de Validação
🧠 Por que Zod para Validação?
- • Schema-first: Define validação uma vez, usa em client e server
- • TypeScript inference: Tipos gerados automaticamente
- • Composição: Reutiliza schemas menores em maiores
- • Mensagens customizadas: Feedback preciso para usuários
import { z } from 'zod';
// 🔧 Schema base para email (reutilizável)
const emailSchema = z
.string()
.min(1, 'Email é obrigatório')
.email('Email inválido')
.max(100, 'Email muito longo');
// 🔒 Schema base para senha (reutilizável)
const passwordSchema = z
.string()
.min(1, 'Senha é obrigatória')
.min(6, 'Senha deve ter pelo menos 6 caracteres')
.max(100, 'Senha muito longa');
// 👤 Schema para cadastro
export const signUpSchema = z.object({
name: z
.string()
.min(1, 'Nome é obrigatório')
.min(2, 'Nome deve ter pelo menos 2 caracteres')
.max(50, 'Nome muito longo')
.regex(/^[a-zA-ZÀ-ÿs]+$/, 'Nome deve conter apenas letras'),
email: emailSchema, // ♻️ Reutilizamos o schema base
password: passwordSchema, // ♻️ Reutilizamos o schema base
confirmPassword: z.string().min(1, 'Confirmação de senha é obrigatória'),
role: z.enum(['customer', 'manager']).default('customer'),
}).refine((data) => data.password === data.confirmPassword, {
// 🔍 Validação customizada para senhas iguais
message: 'As senhas não coincidem',
path: ['confirmPassword'], // Campo onde o erro será mostrado
});
// 🔑 Schema para login (mais simples)
export const signInSchema = z.object({
email: emailSchema, // ♻️ Reutilizamos
password: passwordSchema, // ♻️ Reutilizamos
});
// 📧 Schema para reset de senha
export const requestPasswordResetSchema = z.object({
email: emailSchema, // ♻️ Reutilizamos
});
// 🔄 Schema para nova senha
export const resetPasswordSchema = z.object({
token: z.string().min(1, 'Token inválido'),
password: passwordSchema, // ♻️ Reutilizamos
confirmPassword: z.string().min(1, 'Confirmação de senha é obrigatória'),
}).refine((data) => data.password === data.confirmPassword, {
message: 'As senhas não coincidem',
path: ['confirmPassword'],
});
// 🎯 Tipos TypeScript gerados automaticamente pelos schemas
export type SignUpFormData = z.infer<typeof signUpSchema>;
export type SignInFormData = z.infer<typeof signInSchema>;
export type RequestPasswordResetFormData = z.infer<typeof requestPasswordResetSchema>;
export type ResetPasswordFormData = z.infer<typeof resetPasswordSchema>;
// 💡 Exemplo de uso dos tipos:
// function handleSignUp(data: SignUpFormData) {
// // TypeScript sabe que data tem: name, email, password, confirmPassword, role
// console.log(data.name); // ✅ string
// console.log(data.email); // ✅ string (validado como email)
// console.log(data.role); // ✅ 'customer' | 'manager'
// }⚡ src/lib/auth-actions.ts - Server Actions Seguras
🛡️ Por que Server Actions são Mais Seguras?
- • Server-side validation: Validação nunca pode ser bypassada
- • No API exposure: Não expõe endpoints públicos
- • CSRF protection: Built-in no Next.js
- • Rate limiting: Mais fácil implementar throttling
'use server';
import { redirect } from 'next/navigation';
import { api } from '@/lib/api';
import {
signUpSchema,
signInSchema,
requestPasswordResetSchema,
type SignUpFormData,
type SignInFormData,
type RequestPasswordResetFormData,
} from '@/lib/auth-validations';
// 🎯 Tipo de retorno padronizado para Server Actions
interface ActionResult {
success: boolean;
error?: string;
data?: any;
}
// 👤 Server Action para Cadastro
export async function signUpAction(formData: FormData): Promise<ActionResult> {
try {
// 📥 Extrair dados do FormData
const rawData = {
name: formData.get('name') as string,
email: formData.get('email') as string,
password: formData.get('password') as string,
confirmPassword: formData.get('confirmPassword') as string,
role: (formData.get('role') as 'customer' | 'manager') || 'customer',
};
// 🔍 Validação server-side (CRUCIAL - nunca confie apenas no cliente)
const validationResult = signUpSchema.safeParse(rawData);
if (!validationResult.success) {
// 📝 Formatar erros de validação para UI
const errorMessages = validationResult.error.errors
.map((err) => `${err.path.join('.')}: ${err.message}`)
.join(', ');
return {
success: false,
error: errorMessages,
};
}
const validData = validationResult.data;
// 🌐 Tentar criar conta via API
await api.signUp({
name: validData.name,
email: validData.email,
password: validData.password,
role: validData.role,
});
// ✅ Sucesso - redirecionar para dashboard
redirect('/dashboard');
} catch (error) {
console.error('🚨 Erro no cadastro:', error);
// 🎯 Tratamento de erros específicos
if (error instanceof Error) {
if (error.message.includes('email')) {
return {
success: false,
error: 'Este email já está em uso. Tente fazer login.',
};
}
return {
success: false,
error: error.message,
};
}
return {
success: false,
error: 'Erro interno do servidor. Tente novamente.',
};
}
}
// 🔑 Server Action para Login
export async function signInAction(formData: FormData): Promise<ActionResult> {
try {
// 📥 Extrair dados do FormData
const rawData = {
email: formData.get('email') as string,
password: formData.get('password') as string,
};
// 🔍 Validação server-side
const validationResult = signInSchema.safeParse(rawData);
if (!validationResult.success) {
const errorMessages = validationResult.error.errors
.map((err) => `${err.path.join('.')}: ${err.message}`)
.join(', ');
return {
success: false,
error: errorMessages,
};
}
const { email, password } = validationResult.data;
// 🌐 Tentar fazer login via API
await api.signIn(email, password);
// ✅ Sucesso - redirecionar para dashboard
redirect('/dashboard');
} catch (error) {
console.error('🚨 Erro no login:', error);
if (error instanceof Error) {
// 🎯 Padronizar mensagem de erro (não dar dicas sobre contas existentes)
if (error.message.includes('incorretos') ||
error.message.includes('inválido') ||
error.message.includes('404') ||
error.message.includes('401')) {
return {
success: false,
error: 'Email ou senha incorretos.',
};
}
return {
success: false,
error: 'Erro ao fazer login. Tente novamente.',
};
}
return {
success: false,
error: 'Erro interno do servidor. Tente novamente.',
};
}
}
// 📧 Server Action para Solicitação de Reset de Senha
export async function requestPasswordResetAction(
formData: FormData
): Promise<ActionResult> {
try {
const rawData = {
email: formData.get('email') as string,
};
// 🔍 Validação server-side
const validationResult = requestPasswordResetSchema.safeParse(rawData);
if (!validationResult.success) {
const errorMessages = validationResult.error.errors
.map((err) => `${err.path.join('.')}: ${err.message}`)
.join(', ');
return {
success: false,
error: errorMessages,
};
}
const { email } = validationResult.data;
// 🌐 Solicitar reset via API
await api.requestPasswordReset(email);
// ✅ Sempre retornar sucesso (não vazar informações sobre contas existentes)
return {
success: true,
data: { message: 'Se o email existir, você receberá instruções para redefinir sua senha.' },
};
} catch (error) {
console.error('🚨 Erro no reset de senha:', error);
// 🛡️ Por segurança, sempre retornar sucesso (não vazar se email existe)
return {
success: true,
data: { message: 'Se o email existir, você receberá instruções para redefinir sua senha.' },
};
}
}
// 💡 Exemplo de uso em componentes:
//
// <form action={signInAction}>
// <input name="email" type="email" required />
// <input name="password" type="password" required />
// <button type="submit">Entrar</button>
// </form>
//
// Ou com React Hook Form:
//
// const { handleSubmit } = useForm();
// const onSubmit = async (data) => {
// const formData = new FormData();
// Object.entries(data).forEach(([key, value]) => {
// formData.append(key, value);
// });
// const result = await signInAction(formData);
// if (!result.success) {
// setError(result.error);
// }
// };📝 Componente de Formulário de Login
🔥 Features Implementadas:
- • React Hook Form: Performance otimizada com uncontrolled inputs
- • Zod validation: Validação client-side em tempo real
- • Server Actions: Segurança server-side
- • Loading states: UX durante submissão
- • Error handling: Feedback visual para erros
'use client';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { signInSchema, type SignInFormData } from '@/lib/auth-validations';
import { signInAction } from '@/lib/auth-actions';
import { useAuthStore } from '@/store/auth-store';
export function LoginForm() {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [serverError, setServerError] = useState<string | null>(null);
// 🏪 Zustand store para atualizar estado global
const signIn = useAuthStore((state) => state.signIn);
// ⚡ Configuração do React Hook Form com Zod
const {
register, // Registra inputs no formulário
handleSubmit, // Handler para submissão
formState: {
errors, // Erros de validação
isSubmitting, // Estado de submissão
isValid // Se formulário é válido
},
setError, // Definir erros programaticamente
clearErrors, // Limpar erros
} = useForm<SignInFormData>({
resolver: zodResolver(signInSchema), // 🔍 Integração com Zod
mode: 'onChange', // Validar ao digitar
defaultValues: {
email: '',
password: '',
},
});
// 🎯 Handler de submissão (híbrido: client + server)
const onSubmit = async (data: SignInFormData) => {
try {
setIsLoading(true);
setServerError(null);
clearErrors();
// 🔄 Abordagem híbrida: tentar Zustand primeiro, fallback para Server Action
try {
// 1️⃣ Tentar login via Zustand (mais rápido, melhor UX)
await signIn(data.email, data.password);
router.push('/dashboard');
return;
} catch (zustandError) {
console.warn('⚠️ Login via Zustand falhou, tentando Server Action...');
// 2️⃣ Fallback: usar Server Action
const formData = new FormData();
formData.append('email', data.email);
formData.append('password', data.password);
const result = await signInAction(formData);
if (!result.success) {
throw new Error(result.error || 'Erro ao fazer login');
}
// Se chegou aqui, Server Action redirecionou com sucesso
}
} catch (error) {
// 🚨 Tratamento de erros
console.error('🚨 Erro no login:', error);
if (error instanceof Error) {
// 🎯 Classificar tipos de erro para melhor UX
if (error.message.includes('network') || error.message.includes('fetch')) {
setServerError('Erro de conexão. Verifique sua internet e tente novamente.');
} else if (error.message.includes('401') || error.message.includes('incorretos')) {
setError('email', {
type: 'manual',
message: 'Email ou senha incorretos'
});
setError('password', {
type: 'manual',
message: 'Email ou senha incorretos'
});
} else {
setServerError(error.message);
}
} else {
setServerError('Erro inesperado. Tente novamente.');
}
} finally {
setIsLoading(false);
}
};
return (
<div className="w-full max-w-md mx-auto p-6 bg-bg-1 rounded-lg border border-border">
<h2 className="text-2xl font-bold text-foreground mb-6 text-center">
Entrar no Restaurantix
</h2>
{/* 🚨 Exibir erro do servidor */}
{serverError && (
<Alert className="mb-4 border-red-500 bg-red-500/10">
<AlertDescription className="text-red-400">
{serverError}
</AlertDescription>
</Alert>
)}
{/* 📝 Formulário */}
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
{/* 📧 Campo Email */}
<div>
<Label htmlFor="email" className="text-foreground">
Email
</Label>
<Input
id="email"
type="email"
placeholder="seu@email.com"
className="bg-bg-2 border-border text-foreground"
{...register('email')} // 🔗 Registra no React Hook Form
disabled={isLoading}
/>
{/* ❌ Exibir erro de validação */}
{errors.email && (
<p className="text-red-400 text-sm mt-1">
{errors.email.message}
</p>
)}
</div>
{/* 🔒 Campo Senha */}
<div>
<Label htmlFor="password" className="text-foreground">
Senha
</Label>
<Input
id="password"
type="password"
placeholder="sua senha"
className="bg-bg-2 border-border text-foreground"
{...register('password')} // 🔗 Registra no React Hook Form
disabled={isLoading}
/>
{/* ❌ Exibir erro de validação */}
{errors.password && (
<p className="text-red-400 text-sm mt-1">
{errors.password.message}
</p>
)}
</div>
{/* 🚀 Botão de Submit */}
<Button
type="submit"
className="w-full bg-blue-600 hover:bg-blue-700"
disabled={isLoading || isSubmitting || !isValid}
>
{isLoading ? (
<>
<div className="animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full mr-2" />
Entrando...
</>
) : (
'Entrar'
)}
</Button>
</form>
{/* 🔗 Links auxiliares */}
<div className="mt-4 text-center space-y-2">
<p className="text-fg-muted text-sm">
Esqueceu sua senha?{' '}
<button
onClick={() => router.push('/forgot-password')}
className="text-blue-400 hover:underline"
>
Clique aqui
</button>
</p>
<p className="text-fg-muted text-sm">
Não tem conta?{' '}
<button
onClick={() => router.push('/cadastro')}
className="text-blue-400 hover:underline"
>
Cadastre-se
</button>
</p>
</div>
</div>
);
}
// 💡 Principais benefícios desta implementação:
//
// 🎯 Performance:
// - React Hook Form usa uncontrolled inputs (menos re-renders)
// - Validação acontece apenas quando necessário
// - Zustand evita prop drilling
//
// 🛡️ Segurança:
// - Validação client + server (dupla proteção)
// - Server Actions protegem contra CSRF
// - Não vaza informações sobre contas existentes
//
// 🎨 UX:
// - Feedback visual imediato (loading, erros)
// - Validação em tempo real
// - Mensagens de erro claras e acionáveis
//
// 🔧 Manutenibilidade:
// - Schemas reutilizáveis
// - Tipos TypeScript automáticos
// - Separação clara de responsabilidades🏗️ Arquitetura da Validação (Client + Server)
Client-Side (React Hook Form + Zod):
- • Feedback imediato ao usuário
- • Menos requisições desnecessárias
- • Melhor experiência de usuário
- • Validação em tempo real
Server-Side (Server Actions + Zod):
- • Segurança que não pode ser bypassada
- • Proteção contra ataques maliciosos
- • Validação final antes do banco
- • Rate limiting e throttling
✅ Benefícios da Abordagem Híbrida
- 🚀 Performance: Uncontrolled inputs, menos re-renders
- 🔒 Segurança: Validação dupla (client + server)
- 🎨 UX: Feedback imediato e states visuais
- 📱 Acessibilidade: Progressive enhancement
- 🔧 Manutenível: Schemas reutilizáveis
- 🎯 Type-safe: TypeScript end-to-end
- 🧪 Testável: Lógica isolada e pura
- 📈 Escalável: Padrões consistentes
🔄 O Fluxo Completo de Reset de Senha
Um sistema seguro de reset de senha precisa ser impossível de explorar, user-friendly e temporalmente limitado.
Princípios de Segurança:
- • Não vazar informações sobre contas existentes
- • Tokens únicos, criptograficamente seguros
- • Expiração temporal obrigatória
- • Rate limiting para prevenir spam
Experiência do Usuário:
- • Feedback claro sobre o que fazer
- • Email com instruções detalhadas
- • URL de reset que expira
- • Confirmação após reset bem-sucedido
🎯 Fluxo Técnico Passo a Passo
Usuário solicita reset
Envia email no formulário "Esqueci minha senha"
Servidor valida e gera token
Token único (UUID/JWT) com expiração de 15 minutos
Email enviado (sempre)
Mesmo se email não existir (não vazar informações)
Usuário clica no link
Redirecionado para página de nova senha com token
Nova senha definida
Token invalidado, hash da senha atualizado no banco
📧 Formulário "Esqueci Minha Senha"
🎨 Design Patterns Implementados:
- • Information Hiding: Não revela se email existe ou não
- • Progressive Disclosure: Mostra próximos passos após submissão
- • Error Prevention: Validação em tempo real
- • Feedback: Estados visuais claros (loading, success, error)
'use client';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { CheckCircle, Mail, AlertCircle } from 'lucide-react';
import {
requestPasswordResetSchema,
type RequestPasswordResetFormData
} from '@/lib/auth-validations';
import { requestPasswordResetAction } from '@/lib/auth-actions';
export function ForgotPasswordForm() {
const [isLoading, setIsLoading] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [serverError, setServerError] = useState<string | null>(null);
// ⚡ React Hook Form com Zod validation
const {
register,
handleSubmit,
formState: { errors, isValid },
watch,
} = useForm<RequestPasswordResetFormData>({
resolver: zodResolver(requestPasswordResetSchema),
mode: 'onChange', // Validar em tempo real
defaultValues: {
email: '',
},
});
// 👀 Observar valor do email para UX dinâmica
const emailValue = watch('email');
// 🎯 Handler de submissão
const onSubmit = async (data: RequestPasswordResetFormData) => {
try {
setIsLoading(true);
setServerError(null);
// 🌐 Criar FormData para Server Action
const formData = new FormData();
formData.append('email', data.email);
// 📡 Chamar Server Action
const result = await requestPasswordResetAction(formData);
if (result.success) {
// ✅ Sempre mostrar sucesso (segurança)
setIsSuccess(true);
} else {
// ❌ Mostrar erro apenas se for erro de validação
setServerError(result.error || 'Erro ao processar solicitação');
}
} catch (error) {
console.error('🚨 Erro no forgot password:', error);
setServerError('Erro inesperado. Tente novamente.');
} finally {
setIsLoading(false);
}
};
// 🎉 Estado de sucesso
if (isSuccess) {
return (
<div className="w-full max-w-md mx-auto p-6 bg-bg-1 rounded-lg border border-border">
<div className="text-center space-y-4">
{/* ✅ Ícone de sucesso */}
<div className="mx-auto w-16 h-16 bg-green-500/20 rounded-full flex items-center justify-center">
<CheckCircle className="w-8 h-8 text-green-400" />
</div>
{/* 📧 Título e descrição */}
<div className="space-y-2">
<h2 className="text-xl font-bold text-foreground">
Email Enviado!
</h2>
<p className="text-fg-muted text-sm leading-relaxed">
Se existe uma conta com o email <strong className="text-blue-400">{emailValue}</strong>,
você receberá instruções para redefinir sua senha.
</p>
</div>
{/* 🔍 Instruções adicionais */}
<div className="bg-blue-900/20 p-4 rounded-lg border border-blue-400/30 text-left">
<h4 className="text-blue-400 font-semibold mb-2 flex items-center">
<Mail className="w-4 h-4 mr-2" />
Próximos Passos:
</h4>
<ul className="text-fg-muted text-sm space-y-1">
<li>• Verifique sua caixa de entrada</li>
<li>• Clique no link de redefinição</li>
<li>• O link expira em <strong className="text-yellow-400">15 minutos</strong></li>
<li>• Verifique também a pasta de spam</li>
</ul>
</div>
{/* 🔄 Botão para tentar novamente */}
<Button
onClick={() => {
setIsSuccess(false);
setServerError(null);
}}
variant="outline"
className="w-full"
>
Tentar Novamente
</Button>
</div>
</div>
);
}
// 📝 Formulário de solicitação
return (
<div className="w-full max-w-md mx-auto p-6 bg-bg-1 rounded-lg border border-border">
<div className="text-center mb-6">
<h2 className="text-2xl font-bold text-foreground mb-2">
Esqueceu sua senha?
</h2>
<p className="text-fg-muted text-sm">
Digite seu email e enviaremos instruções para redefinir sua senha.
</p>
</div>
{/* 🚨 Erro do servidor */}
{serverError && (
<Alert className="mb-4 border-red-500 bg-red-500/10">
<AlertCircle className="h-4 w-4" />
<AlertDescription className="text-red-400">
{serverError}
</AlertDescription>
</Alert>
)}
{/* 📝 Formulário */}
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div>
<Label htmlFor="email" className="text-foreground">
Email
</Label>
<Input
id="email"
type="email"
placeholder="seu@email.com"
className="bg-bg-2 border-border text-foreground"
{...register('email')}
disabled={isLoading}
/>
{/* ❌ Erro de validação */}
{errors.email && (
<p className="text-red-400 text-sm mt-1">
{errors.email.message}
</p>
)}
</div>
{/* 🚀 Botão de submit */}
<Button
type="submit"
className="w-full bg-orange-600 hover:bg-orange-700"
disabled={isLoading || !isValid}
>
{isLoading ? (
<>
<div className="animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full mr-2" />
Enviando...
</>
) : (
<>
<Mail className="w-4 h-4 mr-2" />
Enviar Instruções
</>
)}
</Button>
</form>
{/* 🔗 Link para voltar ao login */}
<div className="mt-4 text-center">
<p className="text-fg-muted text-sm">
Lembrou da senha?{' '}
<Link href="/login" className="text-blue-400 hover:underline">
Voltar ao login
</Link>
</p>
</div>
</div>
);
}
// 💡 Principais características de segurança:
//
// 🛡️ Information Hiding:
// - Sempre retorna sucesso, independente de email existir
// - Não vaza informações sobre contas na base
//
// ⏰ Time-based Security:
// - Tokens expiram rapidamente (15 min)
// - Rate limiting no servidor previne spam
//
// 🎨 UX Considerations:
// - Estados visuais claros (loading, success, error)
// - Instruções detalhadas sobre próximos passos
// - Feedback sobre o que esperar🔒 Formulário de Nova Senha (Reset)
🔐 Validações de Segurança:
- • Token validation: Verifica se token é válido e não expirou
- • Password strength: Força mínima de senha
- • Confirmation match: Senhas devem coincidir
- • One-time use: Token invalidado após uso
'use client';
import { useState, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useRouter, useSearchParams } from 'next/navigation';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { CheckCircle, Lock, AlertTriangle } from 'lucide-react';
import {
resetPasswordSchema,
type ResetPasswordFormData
} from '@/lib/auth-validations';
import { resetPasswordAction } from '@/lib/auth-actions';
import Link from 'next/link';
export function ResetPasswordForm() {
const router = useRouter();
const searchParams = useSearchParams();
const [isLoading, setIsLoading] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [serverError, setServerError] = useState<string | null>(null);
// 🔍 Extrair token da URL
const token = searchParams.get('token');
// ⚡ React Hook Form com validação robusta
const {
register,
handleSubmit,
formState: { errors, isValid },
watch,
setError,
} = useForm<ResetPasswordFormData>({
resolver: zodResolver(resetPasswordSchema),
mode: 'onChange',
defaultValues: {
token: token || '',
password: '',
confirmPassword: '',
},
});
// 👀 Observar senhas para validação visual
const password = watch('password');
const confirmPassword = watch('confirmPassword');
// 🚨 Verificar se token existe
useEffect(() => {
if (!token) {
setServerError('Token de reset inválido ou expirado.');
}
}, [token]);
// 🎯 Handler de submissão
const onSubmit = async (data: ResetPasswordFormData) => {
try {
setIsLoading(true);
setServerError(null);
// 🌐 Preparar FormData
const formData = new FormData();
formData.append('token', data.token);
formData.append('password', data.password);
formData.append('confirmPassword', data.confirmPassword);
// 📡 Chamar Server Action
const result = await resetPasswordAction(formData);
if (result.success) {
setIsSuccess(true);
} else {
// 🎯 Tratar diferentes tipos de erro
if (result.error?.includes('token')) {
setServerError('Token inválido ou expirado. Solicite um novo reset.');
} else if (result.error?.includes('senha')) {
setError('password', {
type: 'manual',
message: result.error,
});
} else {
setServerError(result.error || 'Erro ao redefinir senha');
}
}
} catch (error) {
console.error('🚨 Erro no reset password:', error);
setServerError('Erro inesperado. Tente novamente.');
} finally {
setIsLoading(false);
}
};
// 🎉 Estado de sucesso
if (isSuccess) {
return (
<div className="w-full max-w-md mx-auto p-6 bg-bg-1 rounded-lg border border-border">
<div className="text-center space-y-4">
<div className="mx-auto w-16 h-16 bg-green-500/20 rounded-full flex items-center justify-center">
<CheckCircle className="w-8 h-8 text-green-400" />
</div>
<div className="space-y-2">
<h2 className="text-xl font-bold text-foreground">
Senha Redefinida!
</h2>
<p className="text-fg-muted text-sm">
Sua senha foi alterada com sucesso. Agora você pode fazer login com sua nova senha.
</p>
</div>
<Button
onClick={() => router.push('/login')}
className="w-full bg-green-600 hover:bg-green-700"
>
Ir para Login
</Button>
</div>
</div>
);
}
// 🚨 Token inválido
if (!token || serverError?.includes('Token')) {
return (
<div className="w-full max-w-md mx-auto p-6 bg-bg-1 rounded-lg border border-border">
<div className="text-center space-y-4">
<div className="mx-auto w-16 h-16 bg-red-500/20 rounded-full flex items-center justify-center">
<AlertTriangle className="w-8 h-8 text-red-400" />
</div>
<div className="space-y-2">
<h2 className="text-xl font-bold text-foreground">
Link Inválido
</h2>
<p className="text-fg-muted text-sm">
Este link de redefinição é inválido ou expirou.
Solicite um novo reset de senha.
</p>
</div>
<Button
onClick={() => router.push('/forgot-password')}
className="w-full bg-orange-600 hover:bg-orange-700"
>
Solicitar Novo Reset
</Button>
</div>
</div>
);
}
// 📝 Formulário de nova senha
return (
<div className="w-full max-w-md mx-auto p-6 bg-bg-1 rounded-lg border border-border">
<div className="text-center mb-6">
<h2 className="text-2xl font-bold text-foreground mb-2">
Nova Senha
</h2>
<p className="text-fg-muted text-sm">
Digite sua nova senha abaixo. Certifique-se de que seja segura.
</p>
</div>
{/* 🚨 Erro do servidor */}
{serverError && !serverError.includes('Token') && (
<Alert className="mb-4 border-red-500 bg-red-500/10">
<AlertTriangle className="h-4 w-4" />
<AlertDescription className="text-red-400">
{serverError}
</AlertDescription>
</Alert>
)}
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
{/* 🔐 Campo Token (hidden) */}
<input
type="hidden"
{...register('token')}
/>
{/* 🔒 Nova senha */}
<div>
<Label htmlFor="password" className="text-foreground">
Nova Senha
</Label>
<Input
id="password"
type="password"
placeholder="Sua nova senha"
className="bg-bg-2 border-border text-foreground"
{...register('password')}
disabled={isLoading}
/>
{errors.password && (
<p className="text-red-400 text-sm mt-1">
{errors.password.message}
</p>
)}
{/* 💪 Indicador de força da senha */}
{password && (
<div className="mt-2">
<div className="flex space-x-1">
<div className={`h-1 flex-1 rounded ${password.length >= 6 ? 'bg-green-500' : 'bg-bg-3'}`} />
<div className={`h-1 flex-1 rounded ${password.length >= 8 ? 'bg-green-500' : 'bg-bg-3'}`} />
<div className={`h-1 flex-1 rounded ${/[A-Z]/.test(password) ? 'bg-green-500' : 'bg-bg-3'}`} />
<div className={`h-1 flex-1 rounded ${/[0-9]/.test(password) ? 'bg-green-500' : 'bg-bg-3'}`} />
</div>
<p className="text-xs text-fg-muted mt-1">
Força da senha: {password.length >= 8 && /[A-Z]/.test(password) && /[0-9]/.test(password) ? 'Forte' : 'Fraca'}
</p>
</div>
)}
</div>
{/* 🔒 Confirmar senha */}
<div>
<Label htmlFor="confirmPassword" className="text-foreground">
Confirmar Nova Senha
</Label>
<Input
id="confirmPassword"
type="password"
placeholder="Digite novamente"
className="bg-bg-2 border-border text-foreground"
{...register('confirmPassword')}
disabled={isLoading}
/>
{errors.confirmPassword && (
<p className="text-red-400 text-sm mt-1">
{errors.confirmPassword.message}
</p>
)}
{/* ✅ Indicador de senhas iguais */}
{password && confirmPassword && (
<p className={`text-xs mt-1 ${password === confirmPassword ? 'text-green-400' : 'text-red-400'}`}>
{password === confirmPassword ? '✅ Senhas coincidem' : '❌ Senhas não coincidem'}
</p>
)}
</div>
<Button
type="submit"
className="w-full bg-blue-600 hover:bg-blue-700"
disabled={isLoading || !isValid}
>
{isLoading ? (
<>
<div className="animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full mr-2" />
Redefinindo...
</>
) : (
<>
<Lock className="w-4 h-4 mr-2" />
Redefinir Senha
</>
)}
</Button>
</form>
</div>
);
}
// 🔐 Características de segurança implementadas:
//
// 🛡️ Token Security:
// - Validação de token obrigatória
// - Tokens de uso único (invalidados após uso)
// - Expiração temporal (15 min)
//
// 🔒 Password Security:
// - Validação de força em tempo real
// - Confirmação obrigatória
// - Hash seguro no servidor
//
// 🎨 UX Excellence:
// - Feedback visual sobre força da senha
// - Estados claros (loading, success, error)
// - Mensagens específicas para cada tipo de erro🔄 Considerações de Segurança vs UX
🛡️ Segurança Primeiro:
- • Nunca revelar se email existe
- • Tokens únicos e não previsíveis
- • Expiração temporal agressiva
- • Rate limiting para prevenir ataques
- • Logs de tentativas suspeitas
🎨 UX Balanceada:
- • Feedback claro sobre próximos passos
- • Instruções detalhadas no email
- • Estados visuais informativos
- • Fallbacks para casos de erro
- • Tempo adequado para completar
✅ Melhores Práticas Implementadas
- 🔐 Security by Design: Segurança como requisito, não adição
- ⏰ Time-bounded: Tokens com expiração apropriada
- 📧 Email Templates: Instruções claras e branded
- 🎯 Error Handling: Mensagens específicas mas seguras
- 🧪 Testável: Fluxos isolados e determinísticos
- 📊 Monitorável: Logs para detecção de ataques
- ♿ Acessível: Compatible com screen readers
- 📱 Responsivo: Funciona em todos dispositivos
src/app/page.tsx
'use client';
import { useAuthStore } from '@/stores/auth-store';
import { useShallow } from 'zustand/react/shallow';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { ArrowRight, ChefHat, Loader2, Users } from 'lucide-react';
import { Button } from '@/components/ui/button';
import Link from 'next/link';
import { redirect } from 'next/navigation';
import { useAuthInit } from '@/hooks/use-auth-init';
export default function HomePage() {
const { user, isLoading, isAuthenticated } = useAuthStore(
useShallow((state) => ({
user: state.user,
isLoading: state.isLoading,
isAuthenticated: state.isAuthenticated,
})),
);
useAuthInit();
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<Loader2 className="h-8 w-8 animate-spin mx-auto mb-4" />
<p>Carregando...</p>
</div>
</div>
);
}
if (isAuthenticated && user) {
redirect('/dashboard');
}
return (
<div className="min-h-screen bg-gradient-to-br from-orange-50 to-red-50">
<main className="container mx-auto px-4 py-16">
<div className="text-center mb-16">
<h1 className="text-5xl font-bold mb-6 bg-gradient-to-r from-orange-600 to-red-600 bg-clip-text text-transparent">
Restaurantix
</h1>
<p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto">
Sistema completo de gestão para restaurantes. Gerencie pedidos, acompanhe métricas e
otimize suas operações.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button asChild size="lg" className="bg-orange-600 hover:bg-orange-700">
<Link href="/sign-in">
Fazer Login
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
<Button asChild variant="outline" size="lg">
<Link href="/sign-up">Criar Conta</Link>
</Button>
</div>
</div>
<div className="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto">
<Card className="border-orange-200">
<CardHeader>
<CardTitle className="flex items-center text-orange-700">
<ChefHat className="h-6 w-6 mr-2" />
Para Gerentes
</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2 text-muted-foreground">
<li>• Gerencie pedidos em tempo real</li>
<li>• Acompanhe métricas de vendas</li>
<li>• Controle estoque e cardápio</li>
<li>• Relatórios detalhados</li>
</ul>
</CardContent>
</Card>
<Card className="border-red-200">
<CardHeader>
<CardTitle className="flex items-center text-red-700">
<Users className="h-6 w-6 mr-2" />
Para Clientes
</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2 text-muted-foreground">
<li>• Faça pedidos online</li>
<li>• Acompanhe status do pedido</li>
<li>• Histórico de compras</li>
<li>• Avaliações e feedback</li>
</ul>
</CardContent>
</Card>
</div>
</main>
</div>
);
}
src/app/sign-in/page.tsx
'use client';
import { Suspense } from 'react';
import { SignInForm } from '@/components/auth/sign-in-form';
import { Card, CardContent } from '@/components/ui/card';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
import { useEffect } from 'react';
import { toast } from 'sonner';
function SignInContent() {
const searchParams = useSearchParams();
useEffect(() => {
const error = searchParams.get('error');
const details = searchParams.get('details');
if (error) {
switch (error) {
case 'missing_cookies':
toast.error('Erro de autenticação: cookies não encontrados. Tente novamente.');
break;
case 'missing_code_or_state':
toast.error('Erro de autenticação: parâmetros inválidos.');
break;
case 'invalid_state':
toast.error('Erro de autenticação: sessão inválida.');
break;
case 'google_error':
toast.error(`Erro do Google: ${details || 'Erro desconhecido'}`);
break;
case 'auth_failed':
toast.error('Falha na autenticação. Tente novamente.');
break;
default:
toast.error('Erro ao fazer login. Tente novamente.');
}
}
}, [searchParams]);
return (
<div className="flex min-h-svh flex-col items-center justify-center bg-muted p-6 md:p-10">
<div className="w-full max-w-sm md:max-w-3xl">
<div className="flex flex-col gap-6">
<Card className="overflow-hidden">
<CardContent className="grid p-0 md:grid-cols-2">
<div className="p-6 md:p-8">
<div className="flex flex-col gap-6">
<div className="flex flex-col items-center text-center">
<h1 className="text-2xl font-bold">Bem-vindo de volta</h1>
<p className="text-balance text-muted-foreground">
Entre na sua conta Restaurantix
</p>
</div>
<SignInForm />
</div>
</div>
<div className="relative hidden bg-muted md:block">
<img
src="https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=800&h=1000&fit=crop"
alt="Interior de restaurante moderno"
className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
/>
</div>
</CardContent>
</Card>
<div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 hover:[&_a]:text-primary">
Ao clicar em continuar, você concorda com nossos{' '}
<Link href="/terms">Termos de Serviço</Link> e{' '}
<Link href="/privacy">Política de Privacidade</Link>.
</div>
</div>
</div>
</div>
);
}
export default function SignInPage() {
return (
<Suspense fallback={<div>Carregando...</div>}>
<SignInContent />
</Suspense>
);
}
src/app/sign-up/page.tsx
'use client';
import { Suspense, useEffect } from 'react';
import { SignUpForm } from '@/components/auth/sign-up-form';
import { useSearchParams } from 'next/navigation';
import { toast } from 'sonner';
function SignUpContent() {
const searchParams = useSearchParams();
useEffect(() => {
const error = searchParams.get('error');
const details = searchParams.get('details');
if (error) {
switch (error) {
case 'missing_cookies':
toast.error('Erro de autenticação: cookies não encontrados. Tente novamente.');
break;
case 'missing_code_or_state':
toast.error('Erro de autenticação: parâmetros inválidos.');
break;
case 'invalid_state':
toast.error('Erro de autenticação: sessão inválida.');
break;
case 'google_error':
toast.error(`Erro do Google: ${details || 'Erro desconhecido'}`);
break;
case 'auth_failed':
toast.error('Falha na autenticação. Tente novamente.');
break;
default:
toast.error('Erro ao fazer cadastro. Tente novamente.');
}
}
}, [searchParams]);
return (
<div className="bg-muted flex min-h-svh flex-col items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm md:max-w-3xl">
<SignUpForm />
</div>
</div>
);
}
export default function SignUpPage() {
return (
<Suspense fallback={<div>Carregando...</div>}>
<SignUpContent />
</Suspense>
);
}
src/app/forgot-password/page.tsx
import { ForgotPasswordForm } from '@/components/auth/forgot-password-form';
export default function ForgotPasswordPage() {
return (
<div className="bg-muted flex min-h-svh flex-col items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm md:max-w-3xl">
<ForgotPasswordForm />
</div>
</div>
);
}
src/app/reset-password/page.tsx
import { Suspense } from 'react';
import { ResetPasswordForm } from '@/components/auth/reset-password-form';
export default function ResetPasswordPage() {
return (
<div className="bg-muted flex min-h-svh flex-col items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm md:max-w-3xl">
<Suspense fallback={<div>Carregando...</div>}>
<ResetPasswordForm />
</Suspense>
</div>
</div>
);
}
src/app/dashboard/page.tsx
import { cookies } from 'next/headers';
async function getData() {
const allCookies = await cookies();
const csrf = allCookies.get('csrf');
const auth = allCookies.get('auth');
console.log(allCookies);
return { csrf, auth };
}
export default async function DashboardPage() {
const data = await getData();
console.log(data);
return <div className="container mx-auto px-4 py-8">Dashboard</div>;
}
Parabéns! Você implementou um sistema de autenticação profissional
Nesta aula, você construiu do zero um sistema de autenticação robusto e seguro, seguindo as melhores práticas da indústria. Este conhecimento é fundamental para qualquer aplicação web moderna e te prepara para projetos reais.
🛡️ Aspectos de Segurança Dominados
- CSRF Protection: Proteção contra ataques Cross-Site Request Forgery
- HttpOnly Cookies: Tokens inacessíveis via JavaScript (anti-XSS)
- Validação Dupla: Client-side + Server-side validation
- Information Hiding: Não vaza dados sobre contas existentes
- Temporal Security: Tokens com expiração e invalidação
⚡ Tecnologias e Padrões Aplicados
- React Hook Form: Formulários performáticos e acessíveis
- Zod Validation: Schema-first validation TypeScript-safe
- Zustand Store: Estado global simples e eficiente
- Server Actions: Server-side logic segura no Next.js
- API Client: Comunicação HTTP robusta com retry logic
Conceitos Fundamentais Aprendidos
🏗️ Arquitetura
- • Separação de responsabilidades
- • Modularização de código
- • Reutilização de componentes
- • Abstração de complexidade
🔒 Segurança
- • Defense in depth
- • Princípio do menor privilégio
- • Fail secure (falhe com segurança)
- • Security by design
🎨 UX/UI
- • Progressive enhancement
- • Loading states
- • Error handling gracioso
- • Feedback visual
📋 Checklist de Implementação Completa
Backend Integration
- ✅ Variáveis de ambiente validadas
- ✅ Route handlers implementados
- ✅ Cookie propagation funcionando
- ✅ CSRF tokens sendo gerenciados
- ✅ Error handling robusto
State Management
- ✅ Zustand store configurado
- ✅ Persistência implementada
- ✅ Actions assíncronas
- ✅ Error boundaries
- ✅ Loading states
Forms & Validation
- ✅ React Hook Form integrado
- ✅ Zod schemas reutilizáveis
- ✅ Server Actions seguras
- ✅ Real-time validation
- ✅ TypeScript safety
Security Features
- ✅ Password reset flow
- ✅ CSRF protection
- ✅ Input sanitization
- ✅ Rate limiting ready
- ✅ Secure by default
Próximos Passos e Melhorias
🚀 Melhorias de Performance
- • Implementar React Query para cache
- • Otimizar re-renders com useMemo
- • Lazy loading de componentes
- • Service Workers para offline
- • Compressão de assets
🔒 Segurança Avançada
- • Implementar 2FA/MFA
- • Rate limiting por IP
- • Login audit logs
- • Detecção de dispositivos
- • Security headers (HSTS, CSP)
💻 Código de Exemplo: Middleware de Autenticação
Bonus: Como implementar um middleware para proteger rotas privadas:
// 📁 src/middleware.ts - Middleware global do Next.js
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
// 🛡️ Rotas que requerem autenticação
const protectedRoutes = [
'/dashboard',
'/profile',
'/settings',
'/admin',
];
// 🔓 Rotas públicas (apenas para usuários não autenticados)
const publicRoutes = [
'/login',
'/signup',
'/forgot-password',
'/reset-password',
];
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 🍪 Verificar se tem cookie de autenticação
const authCookie = request.cookies.get('auth-token');
const isAuthenticated = !!authCookie?.value;
// 🔍 Verificar se é rota protegida
const isProtectedRoute = protectedRoutes.some(route =>
pathname.startsWith(route)
);
// 🔍 Verificar se é rota pública
const isPublicRoute = publicRoutes.some(route =>
pathname.startsWith(route)
);
// 🚫 Redirecionar não autenticados de rotas protegidas
if (isProtectedRoute && !isAuthenticated) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('redirect', pathname); // Salvar destino original
return NextResponse.redirect(loginUrl);
}
// 🔄 Redirecionar autenticados de rotas públicas
if (isPublicRoute && isAuthenticated) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
// ✅ Permitir acesso
return NextResponse.next();
}
// ⚙️ Configuração - quais rotas o middleware deve processar
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
// 💡 Como usar no componente:
//
// export default function ProtectedPage() {
// const { user, isLoading } = useAuthStore();
//
// if (isLoading) return <LoadingSpinner />;
//
// // Middleware já garantiu que user existe aqui
// return <DashboardContent user={user} />;
// }🎯 O que Você Conquistou Nesta Aula
🎖️ Habilidades Técnicas
- • Arquitetura de autenticação segura
- • Integração de múltiplas tecnologias
- • Padrões de segurança da indústria
- • TypeScript avançado
- • Error handling profissional
🧠 Mindset de Desenvolvedor
- • Pensamento em segurança primeiro
- • Planejamento de arquitetura
- • Código limpo e manutenível
- • Experiência do usuário
- • Debugging sistemático
🚀 Continue Sua Jornada
Você agora tem as bases sólidas para construir sistemas de autenticação profissionais. Este conhecimento é transferível para qualquer stack tecnológica e te coloca num nível avançado de desenvolvimento web.
🎯 Sua Jornada no Módulo 2: Fundamentos Avançados
Aula 1
Fundamentos
Aula 2
Roteamento
Aula 3
UI/UX
Aula 4
Autenticação
Aula 3: Componentes e Styling
Construção de interfaces modernas com Shadcn/UI, Tailwind CSS e componentes reutilizáveis. Design system profissional.
Aula 5: Database & Prisma ORM
Integração com banco de dados PostgreSQL usando Prisma ORM. Migrations, schemas e operações CRUD avançadas.
Dicas para Maximizar seu Aprendizado
Pratique Ativamente
Não apenas assista. Implemente cada conceito em seu projeto. A programação se aprende fazendo.
Questione Tudo
Por que esta abordagem? Existem alternativas? Questionamentos levam a compreensão profunda.
Compartilhe
Ensine outros ou discuta conceitos em comunidades. Ensinar é a melhor forma de consolidar conhecimento.
Progresso do Módulo 2
4 de 8 aulas concluídas
🎯 Próximos tópicos: Database, Deploy, Testing, Performance