Fundamentos do Next.js 15

0/26 aulas0%
pratica

Implementando Autenticação Básica com Server Actions

Aprenda sobre implementando autenticação básica com server actions

55 min
Aula 3 de 6

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:

  1. Fazer login com credenciais simuladas.
  2. Manter uma sessão usando cookies HTTP.
  3. Fazer logout para encerrar a sessão.
  4. 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:

  1. Formulário de Login: Um componente React com um formulário de email e senha.
  2. Submissão: Ao submeter o formulário, os dados são enviados para um Server Action.
  3. Validação no Servidor: O Server Action recebe as credenciais, as valida e (neste exemplo) compara com um usuário "mock".
  4. 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.
  5. Redirecionamento: O usuário é redirecionado para uma página protegida (ex: /dashboard).
  6. 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() de next/headers: Permite ler e definir cookies HTTP no servidor. Essencial para o gerenciamento de sessão.
  • redirect() de next/navigation: Permite redirecionar o usuário para outra rota no servidor.
  • revalidatePath() de next/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 /dashboard diretamente 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.

Desafio 1: Implementar Registro de Usuário 📝

  • Crie uma nova página app/register/page.tsx com um formulário para registro (email, senha, confirmar senha).
  • Adicione um novo Server Action register em lib/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.tsx para usar useFormState para gerenciar e exibir mensagens de erro retornadas pelo Server Action login.

    // 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ção verifyCredentials para simular o uso de hashing de senhas.

    • Sugestão: Em vez de comparar password diretamente, simule uma função hashPassword(password) e comparePassword(password, hashedPassword).
    • Você pode usar uma biblioteca como bcrypt (instale com npm install bcrypt ou yarn add bcrypt) para um hashing real, ou apenas simular com um if/else mais 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;
    }
  • 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!

© 2025 Escola All Dev. Todos os direitos reservados.

Implementando Autenticação Básica com Server Actions - Fundamentos do Next.js 15 | escola.all.dev.br