Implementação completa de autenticação com email/senha, CSRF tokens, cookies seguros e integração com a API do Restaurantix.
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.
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
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
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.
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.
💡 Por que validar variáveis de ambiente?
// 📁 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
🤔 Por que usar Route Handlers como proxy?
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
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"
// }
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"
// }
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"
// }
🧠 O que torna este cliente "inteligente"?
/* 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()
🔧 Pacotes Necessários:
npm install zustand
npm install @types/js-cookie js-cookie
🧠 Conceitos Implementados no Store:
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable no-unused-vars */
'use client';
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,
// }))
// );
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.
📖 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('/');
};
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
🧠 Por que Zod para Validação?
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'
// }
🛡️ Por que Server Actions são Mais Seguras?
'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);
// }
// };
🔥 Features Implementadas:
'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-gray-900 rounded-lg border border-gray-700">
<h2 className="text-2xl font-bold text-white 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-white">
Email
</Label>
<Input
id="email"
type="email"
placeholder="seu@email.com"
className="bg-gray-800 border-gray-600 text-white"
{...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-white">
Senha
</Label>
<Input
id="password"
type="password"
placeholder="sua senha"
className="bg-gray-800 border-gray-600 text-white"
{...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-gray-400 text-sm">
Esqueceu sua senha?{' '}
<button
onClick={() => router.push('/forgot-password')}
className="text-blue-400 hover:underline"
>
Clique aqui
</button>
</p>
<p className="text-gray-400 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
Client-Side (React Hook Form + Zod):
Server-Side (Server Actions + Zod):
Um sistema seguro de reset de senha precisa ser impossível de explorar, user-friendly e temporalmente limitado.
Princípios de Segurança:
Experiência do Usuário:
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
🎨 Design Patterns Implementados:
'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-gray-900 rounded-lg border border-gray-700">
<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-white">
Email Enviado!
</h2>
<p className="text-gray-300 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-gray-300 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-gray-900 rounded-lg border border-gray-700">
<div className="text-center mb-6">
<h2 className="text-2xl font-bold text-white mb-2">
Esqueceu sua senha?
</h2>
<p className="text-gray-400 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-white">
Email
</Label>
<Input
id="email"
type="email"
placeholder="seu@email.com"
className="bg-gray-800 border-gray-600 text-white"
{...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-gray-400 text-sm">
Lembrou da senha?{' '}
<a href="/login" className="text-blue-400 hover:underline">
Voltar ao login
</a>
</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
🔐 Validações de Segurança:
'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';
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-gray-900 rounded-lg border border-gray-700">
<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-white">
Senha Redefinida!
</h2>
<p className="text-gray-300 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-gray-900 rounded-lg border border-gray-700">
<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-white">
Link Inválido
</h2>
<p className="text-gray-300 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-gray-900 rounded-lg border border-gray-700">
<div className="text-center mb-6">
<h2 className="text-2xl font-bold text-white mb-2">
Nova Senha
</h2>
<p className="text-gray-400 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-white">
Nova Senha
</Label>
<Input
id="password"
type="password"
placeholder="Sua nova senha"
className="bg-gray-800 border-gray-600 text-white"
{...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-gray-600'}`} />
<div className={`h-1 flex-1 rounded ${password.length >= 8 ? 'bg-green-500' : 'bg-gray-600'}`} />
<div className={`h-1 flex-1 rounded ${/[A-Z]/.test(password) ? 'bg-green-500' : 'bg-gray-600'}`} />
<div className={`h-1 flex-1 rounded ${/[0-9]/.test(password) ? 'bg-green-500' : 'bg-gray-600'}`} />
</div>
<p className="text-xs text-gray-400 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-white">
Confirmar Nova Senha
</Label>
<Input
id="confirmPassword"
type="password"
placeholder="Digite novamente"
className="bg-gray-800 border-gray-600 text-white"
{...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
🛡️ Segurança Primeiro:
🎨 UX Balanceada:
'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>
);
}
'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>
);
}
'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>
);
}
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>
);
}
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>
);
}
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>;
}
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.
🏗️ Arquitetura
🔒 Segurança
🎨 UX/UI
🚀 Melhorias de Performance
🔒 Segurança Avançada
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} />;
// }
🎖️ Habilidades Técnicas
🧠 Mindset de Desenvolvedor
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.
Aula 1
Fundamentos
Aula 2
Roteamento
Aula 3
UI/UX
Aula 4
Autenticação
Construção de interfaces modernas com Shadcn/UI, Tailwind CSS e componentes reutilizáveis. Design system profissional.
Integração com banco de dados PostgreSQL usando Prisma ORM. Migrations, schemas e operações CRUD avançadas.
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