Fundamentos do Next.js 15
Implementando Autenticação Básica com Server Actions
Aprenda sobre implementando autenticação básica com server actions
Implementando Autenticação Básica com Server Actions ✨
Bem-vindos à aula prática de implementação de autenticação básica para o nosso projeto final! Nesta etapa crucial, vamos integrar a funcionalidade de login e logout usando a poderosa ferramenta do Next.js: os Server Actions.
A autenticação é a espinha dorsal de qualquer aplicação full-stack que lida com dados de usuário, garantindo que apenas usuários autorizados possam acessar recursos específicos. Com Next.js 15, os Server Actions simplificam drasticamente a maneira como lidamos com a lógica de backend, permitindo que você escreva código de servidor diretamente em seus componentes ou arquivos de ação, com segurança e eficiência.
1. Introdução: Autenticação com Server Actions 🚀
Nesta aula, nosso objetivo é construir um sistema de autenticação básico que permita aos usuários:
- Fazer login com credenciais simuladas.
- Manter uma sessão usando cookies HTTP.
- Fazer logout para encerrar a sessão.
- Proteger uma rota para que apenas usuários autenticados possam acessá-la.
Usaremos os Server Actions para processar as submissões do formulário de login e gerenciar o estado da sessão no servidor, tudo de forma transparente e performática.
2. Explicação Detalhada com Exemplos 💡
O que são Server Actions?
Server Actions são funções assíncronas que são executadas no servidor. Elas podem ser chamadas diretamente de componentes React (client ou server components) e são ideais para lidar com mutações de dados e operações de servidor, como:
- Submissão de formulários.
- Atualização de banco de dados.
- Autenticação e autorização.
- Interação com APIs externas.
A grande vantagem é que o Next.js lida com toda a infraestrutura de rede, serialização e revalidação de cache, permitindo que você se concentre na lógica de negócios.
Fluxo de Autenticação Básico
Nosso fluxo será o seguinte:
- Formulário de Login: Um componente React com um formulário de email e senha.
- Submissão: Ao submeter o formulário, os dados são enviados para um Server Action.
- Validação no Servidor: O Server Action recebe as credenciais, as valida e (neste exemplo) compara com um usuário "mock".
- Gerenciamento de Sessão: Se as credenciais forem válidas, um cookie de sessão é criado e enviado de volta ao navegador do usuário. Este cookie será usado para identificar o usuário em requisições subsequentes.
- Redirecionamento: O usuário é redirecionado para uma página protegida (ex:
/dashboard). - Logout: Um Server Action de logout limpa o cookie de sessão, efetivamente encerrando a sessão do usuário.
Ferramentas Essenciais
'use server': Diretiva que marca uma função ou um arquivo inteiro como um Server Action.FormData: Objeto que contém os dados submetidos por um formulário HTML.cookies()denext/headers: Permite ler e definir cookies HTTP no servidor. Essencial para o gerenciamento de sessão.redirect()denext/navigation: Permite redirecionar o usuário para outra rota no servidor.revalidatePath()denext/cache: (Opcional, mas útil) Para revalidar o cache de uma rota após uma ação.
3. Código de Exemplo Oficial (Adaptado para Autenticação) 🚀
Vamos criar uma estrutura de arquivos para organizar nossa lógica de autenticação.
3.1. lib/auth.ts - Gerenciamento de Sessão
Este arquivo conterá funções auxiliares para gerenciar o cookie de sessão.
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
const SESSION_COOKIE_NAME = 'user_session';
const MOCK_USER = {
email: 'user@example.com',
password: 'password123', // Em um cenário real, use hashing de senhas!
name: 'Usuário de Teste',
};
// Em um cenário real, você buscaria o usuário no banco de dados e validaria a senha.
async function verifyCredentials(email: string, password: string) {
if (email === MOCK_USER.email && password === MOCK_USER.password) {
return { id: '123', email: MOCK_USER.email, name: MOCK_USER.name };
}
return null;
}
export async function createSession(userId: string, email: string) {
// Em um cenário real, você geraria um token de sessão seguro (JWT, etc.)
// e o armazenaria no lado do servidor (banco de dados, Redis).
// Para este exemplo, usaremos um valor simples.
const sessionToken = `${userId}-${email}-${Date.now()}`;
cookies().set(SESSION_COOKIE_NAME, sessionToken, {
httpOnly: true, // Impede acesso via JavaScript no cliente
secure: process.env.NODE_ENV === 'production', // Apenas via HTTPS em produção
maxAge: 60 * 60 * 24 * 7, // 1 semana de validade
path: '/', // Válido para toda a aplicação
sameSite: 'lax', // Proteção CSRF
});
}
export async function deleteSession() {
cookies().delete(SESSION_COOKIE_NAME);
}
export async function getSession() {
const sessionToken = cookies().get(SESSION_COOKIE_NAME)?.value;
if (!sessionToken) {
return null;
}
// Em um cenário real, você validaria o token de sessão com seu armazenamento de sessão
// e retornaria os dados do usuário associados.
// Para este exemplo, apenas verificamos se o token existe.
const [userId, email] = sessionToken.split('-');
if (userId && email) {
return { id: userId, email: email, name: MOCK_USER.name }; // Retorna dados mockados para o exemplo
}
return null;
}
export async function requireAuth() {
const session = await getSession();
if (!session) {
redirect('/login');
}
return session;
}3.2. lib/actions.ts - Server Actions de Autenticação
Este arquivo conterá os Server Actions que serão chamados diretamente dos formulários.
'use server'; // Marca todo o arquivo como Server Actions
import { redirect } from 'next/navigation';
import { createSession, deleteSession, verifyCredentials } from '@/lib/auth'; // Ajuste o caminho se necessário
export async function login(formData: FormData) {
const email = formData.get('email') as string;
const password = formData.get('password') as string;
if (!email || !password) {
return { error: 'Por favor, preencha todos os campos.' };
}
const user = await verifyCredentials(email, password);
if (!user) {
return { error: 'Credenciais inválidas.' };
}
await createSession(user.id, user.email);
redirect('/dashboard'); // Redireciona para a página protegida
}
export async function logout() {
await deleteSession();
redirect('/login'); // Redireciona para a página de login
}3.3. app/login/page.tsx - Página de Login
import { login } from '@/lib/actions';
import { getSession } from '@/lib/auth';
import { redirect } from 'next/navigation';
export default async function LoginPage() {
const session = await getSession();
if (session) {
redirect('/dashboard'); // Se já estiver logado, redireciona para o dashboard
}
return (
<div className="flex min-h-screen items-center justify-center bg-gray-100">
<div className="w-full max-w-md rounded-lg bg-white p-8 shadow-md">
<h1 className="mb-6 text-center text-3xl font-bold text-gray-800">Login</h1>
<form action={login} className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
Email
</label>
<input
id="email"
name="email"
type="email"
required
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
placeholder="seu@email.com"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
Senha
</label>
<input
id="password"
name="password"
type="password"
required
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
placeholder="********"
/>
</div>
{/* Exemplo simples de exibição de erro. Em um cenário real, você usaria `useFormState` ou `useFormStatus` */}
{/* para gerenciar o estado do formulário e exibir erros de forma mais robusta. */}
{/* Por simplicidade neste exemplo, o erro é retornado e não exibido diretamente aqui. */}
{/* Para um exemplo mais completo com erros, veja o desafio 2. */}
<button
type="submit"
className="flex w-full justify-center rounded-md border border-transparent bg-indigo-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
Entrar
</button>
</form>
</div>
</div>
);
}3.4. app/dashboard/page.tsx - Página Protegida
import { logout } from '@/lib/actions';
import { requireAuth } from '@/lib/auth';
import Link from 'next/link';
export default async function DashboardPage() {
const session = await requireAuth(); // Garante que o usuário esteja autenticado
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-gray-50 p-4">
<div className="w-full max-w-2xl rounded-lg bg-white p-8 shadow-md">
<h1 className="mb-4 text-center text-4xl font-extrabold text-indigo-700">Bem-vindo ao Dashboard!</h1>
<p className="mb-6 text-center text-lg text-gray-700">
Você está logado como: <span className="font-semibold">{session.name} ({session.email})</span>
</p>
<div className="flex flex-col items-center space-y-4">
<Link href="/" className="text-indigo-600 hover:underline">
Voltar para a Home
</Link>
<form action={logout}>
<button
type="submit"
className="rounded-md border border-transparent bg-red-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
>
Sair
</button>
</form>
</div>
</div>
</div>
);
}3.5. app/page.tsx - Página Inicial (Opcional)
Para testar a navegação.
import Link from 'next/link';
export default function HomePage() {
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-gray-50 p-4">
<h1 className="mb-6 text-5xl font-extrabold text-gray-900">Página Inicial</h1>
<p className="mb-8 text-xl text-gray-700">
Explore a aplicação.
</p>
<div className="flex space-x-4">
<Link href="/login" className="rounded-md bg-indigo-600 px-6 py-3 text-lg font-medium text-white shadow-lg hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
Ir para Login
</Link>
<Link href="/dashboard" className="rounded-md bg-green-600 px-6 py-3 text-lg font-medium text-white shadow-lg hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2">
Ir para Dashboard (Protegido)
</Link>
</div>
</div>
);
}4. Exercícios/Desafios Práticos ✅
Agora é a sua vez de colocar a mão na massa e aprimorar este sistema!
Tarefas Iniciais (Configuração)
- Crie os arquivos e pastas conforme a estrutura acima (
lib/auth.ts,lib/actions.ts,app/login/page.tsx,app/dashboard/page.tsx,app/page.tsx). - Adicione os estilos básicos do Tailwind CSS ou outro framework de sua preferência para que as páginas fiquem visualmente apresentáveis.
- Teste o fluxo de login e logout usando as credenciais mockadas (
user@example.com/password123).- Tente acessar
/dashboarddiretamente sem login. Você deve ser redirecionado para/login. - Faça login e verifique se você é redirecionado para
/dashboard. - Clique em "Sair" e verifique se você é redirecionado para
/login.
- Tente acessar
Desafio 1: Implementar Registro de Usuário 📝
- Crie uma nova página
app/register/page.tsxcom um formulário para registro (email, senha, confirmar senha). - Adicione um novo Server Action
registeremlib/actions.ts. - Este Server Action deve:
- Receber os dados do formulário.
- Validar se as senhas são iguais e se o email não está em uso (mock, claro).
- Se válido, "criar" um novo usuário (adicione a um array mockado ou apenas simule o sucesso).
- Após o registro bem-sucedido, redirecionar o usuário para a página de login ou fazer login automaticamente.
- Adicione um link para a página de registro na página de login.
Desafio 2: Tratamento de Erros Aprimorado com useFormState ⚠️
O exemplo atual não exibe mensagens de erro do Server Action no formulário. Para fazer isso de forma reativa no cliente, você pode usar o hook useFormState do React.
-
Modifique
app/login/page.tsxpara usaruseFormStatepara gerenciar e exibir mensagens de erro retornadas pelo Server Actionlogin.// Exemplo de como usar useFormState no componente de login 'use client'; // Deve ser um Client Component para usar hooks import { useFormState } from 'react-dom'; import { login } from '@/lib/actions'; import { useEffect } from 'react'; // Para lidar com redirecionamento após sucesso const initialState = { error: undefined, }; export default function LoginForm() { const [state, formAction] = useFormState(login, initialState); // Você pode adicionar lógica de redirecionamento aqui se o `login` action // não redirecionar diretamente, ou para lidar com outros estados. // Neste caso, o `login` action já redireciona no servidor. // Este `useEffect` seria mais útil se você quisesse fazer algo no cliente // após um sucesso que não seja um redirect (ex: mostrar um toast). return ( <form action={formAction} className="space-y-4"> {/* ... seus campos de email e senha ... */} {state?.error && ( <p className="text-sm text-red-600">{state.error}</p> )} <button type="submit" /* ... */> Entrar </button> </form> ); } -
(Opcional) Aplique a mesma técnica para a página de registro, exibindo erros de validação (ex: "Senhas não conferem", "Email já registrado").
Desafio 3: Hashing de Senhas (Simulado) 🔒
Em um ambiente de produção, as senhas NUNCA devem ser armazenadas em texto simples.
-
No arquivo
lib/auth.ts, modifique a funçãoverifyCredentialspara simular o uso de hashing de senhas.- Sugestão: Em vez de comparar
passworddiretamente, simule uma funçãohashPassword(password)ecomparePassword(password, hashedPassword). - Você pode usar uma biblioteca como
bcrypt(instale comnpm install bcryptouyarn add bcrypt) para um hashing real, ou apenas simular com umif/elsemais complexo para este exercício.
// Exemplo com bcrypt (instale antes: npm install bcrypt) import bcrypt from 'bcrypt'; // ... dentro de lib/auth.ts const MOCK_USER_HASHED = { email: 'user@example.com', passwordHash: '$2b$10$abcdefghijklmnopqrstuvwx.yzabcdefghijklmno', // Exemplo de hash para 'password123' name: 'Usuário de Teste', }; // Gerar um hash para 'password123' (execute uma vez para obter o hash) // bcrypt.hash('password123', 10).then(hash => console.log(hash)); async function verifyCredentials(email: string, password: string) { if (email === MOCK_USER_HASHED.email) { const passwordMatch = await bcrypt.compare(password, MOCK_USER_HASHED.passwordHash); if (passwordMatch) { return { id: '123', email: MOCK_USER_HASHED.email, name: MOCK_USER_HASHED.name }; } } return null; } - Sugestão: Em vez de comparar
-
Adapte a lógica de registro para "hashear" a senha antes de "armazená-la".
5. Resumo e Próximos Passos 📚
Nesta aula, você aprendeu a implementar um sistema de autenticação básico em Next.js 15 usando Server Actions. Cobrimos:
- A importância e o funcionamento dos Server Actions para lógica de servidor.
- Como gerenciar sessões usando cookies HTTP.
- A criação de formulários que interagem diretamente com Server Actions.
- Proteção de rotas simples baseada em sessão.
Este é um excelente ponto de partida para a autenticação em seu projeto full-stack. Para aprimorar ainda mais, considere os seguintes passos:
- Integração com Banco de Dados: Substitua os usuários mockados por um banco de dados real (PostgreSQL, MongoDB, etc.) e um ORM (Prisma, DrizzleORM).
- Bibliotecas de Autenticação: Explore soluções mais robustas como Auth.js (NextAuth.js) para lidar com provedores de OAuth, gerenciamento de sessões avançado e muito mais.
- Middleware de Autenticação: Para uma proteção de rota mais centralizada e eficiente, implemente um middleware no Next.js para verificar a autenticação em todas as requisições para rotas protegidas.
- Token JWT: Em vez de um token de sessão simples, use JSON Web Tokens (JWTs) para sessões stateless.
- Autorização (RBAC): Implemente controle de acesso baseado em papéis (Role-Based Access Control - RBAC) para gerenciar diferentes níveis de permissão de usuário.
Continue construindo e explorando as capacidades do Next.js para criar aplicações web poderosas e seguras!