DEV Community

g-pg
g-pg

Posted on

NextJS - Criando um site fullstack com autenticação e database

Introdução

Neste artigo quero ensinar você a criar uma aplicação fullstack com NextJS. Registro, login, armazenamento de dados… tudo o que vem no pacote! Será, mais especificamente, uma aplicação CRUD (Create, Read, Update and Delete), isto é, um app que permite ao usuário criar, ler, atualizar e deletar informações guardadas em um servidor.

Decidi escrevê-lo porque, quando pesquisei pela primeira vez sobre o assunto, me deparei com tutoriais “para iniciantes” que atiravam no aluno inúmeros nomes e ferramentas sem justificar as escolhas ou explicar adequadamente que função elas teriam na aplicação.

“Então você instala esse pacote e escreve essa linha aqui e agora o usuário pode logar!”

Ok, está funcionando, MAS EU NÃO ENTENDI /[bad-word]/g NENHUMA!

Compreendendo a dor de quem também está começando, tentarei ser o mais didático possível, explicando cada uma das partes móveis que eu introduzir.

Ao mesmo tempo, aviso que o propósito é criar o esqueleto de uma aplicação fullstack. Vou utilizar como base o conceito de uma lista de tarefas, mas você pode criar o que quiser enquanto lê o artigo.

Levando isso em conta, não me preocuparei com detalhes de funcionamento do React nem com estilização de componentes. Você vai aprender o que interessa: criar um banco de dados e autenticar o usuário.

O que eu preciso saber?

  • Conhecimento básico para intermediário de React e Next.

  • O básico da criação de rotas de API em Javascript.

  • O básico de requests de APIs com Javascript.

O primeiro requisito é essencial. Você pode tentar seguir sem os outros dois, mas sugiro que dê uma olhada no assunto antes de seguir se ainda não tem ideia de como eles funcionam.

O que utilizaremos?

  • NextJS

  • NextAuth

  • Prisma

  • MongoDB

  • BCrypt

  • Axios

  • SWR

Ambientação

Eu gosto de ir direto ao ponto, mas é preciso compreender minimamente as principais ferramentas que nós utilizaremos. Como afirmei acima, existem inúmeros tutoriais de criação de aplicações semelhantes à que criaremos aqui. Mas a grande maioria peca por não justificar a escolha nem explicar a natureza dos instrumentos necessários para fazê-la funcionar.

Sugiro, portanto, que você tenha paciência para se ambientar lendo os capítulos desta seção antes de pular para a parte prática.

As principais ferramentas

Nós queremos criar um sistema que permitirá ao usuário criar uma conta, salvar informações nessa conta e, depois, logar nessa conta a qualquer tempo. A nossa aplicação, portanto, tem três necessidades.

1. Um local para guardar informações

Precisamos de um banco de dados, ou database. Isso é fácil… mas nem tanto. Aqui já nos deparamos com um dilema. Queremos uma database SQL ou NoSQL? O que é isso?

SQL significa Structured Query Language, ou seja, SQL é primeiramente uma linguagem de programação. Dizemos que uma database é SQL quando todas as consultas e manipulações de dados são feitas em SQL.

A linguagem foi criada na década de 1970 e é até hoje padrão da indústria. Não por acaso. O modelo de organização utilizado em bancos SQL (modelo relacional) ainda é efetivo e relevante em praticamente qualquer cenário.

Bancos relacionais estruturam os dados em tabelas, com colunas representando propriedades e linhas representando itens individuais nos quais podemos declarar alguma relação com outros itens. Se quero organizar uma coleção de guitarras, posso criar uma nova tabela GUITARS e adicionar colunas para classificar cada item da minha coleção:

A sintaxe também é relativamente simples: CREATE TABLE cria uma nova tabela, SELECT TABLE seleciona uma tabela para realizarmos as pesquisas, ou queries.

Tudo isso foi dito para situar o caro leitor, porque nós não utilizaremos SQL. Ao invés disso, criaremos a nossa database em um modelo NoSQL, que significa Not Only SQL.

E faremos assim por um ótimo motivo. É mais fácil para quem vem do front-end. O modelo relacional não só possui certas peculiaridades e conceitos (como a criação de relações entre as chaves da tabelas) que merecem artigos individuais, como ele utiliza… SQL. Outra linguagem. Com modelos NoSQL, como veremos, podemos fazer uso de algo que já conhecemos muito bem: Javascript.

E o que são modelos NoSQL? Eles surgiram no fim década de 2000 como alternativas ao modelo tradicional. Não são organizados em tabelas e linhas, mas possuem outros tipos de estrutura, como Documents e Graphs.

Nós utilizaremos o modelo de Documents, que nada mais é que o velho conhecido objeto JSON (você vai ouvir falar em BSON em alguns casos, inclusive no nosso, mas saiba que as maiores diferenças são, em favor do BSON, melhor performance e maior suporte a certos tipos de valores. A sintaxe é a mesma).

Como esse modelo funciona? Ao invés de tabelas com linhas e colunas, temos Collections com Documents. Cada Collection é como uma tabela, e cada linha como um Document. Se acima criamos uma tabela para organizar as guitarras, com o novo modelo criaremos uma Collection. Se cada linha na tabela correspondia a uma guitarra individual, no novo modelo cada guitarra corresponderá a um Document. Ao final, os mesmos dados ficariam organizados assim:

//Collection Guitars
//Você vai entender o que são esses ObjectId mais à frente.

{
 "id": 1,
 "name": "SG",
 "brand": "Gibson",
 "color": "Cherry",
},
{
 "id": 2,
 "name": "Telecaster",
 "brand": "Fender",
 "color": "Sunburst",
}
Enter fullscreen mode Exit fullscreen mode

Assim é fácil visualizar, para quem já conhece a sintaxe de objetos do Javascript, os dados de cada entidade no banco de dados.

Nós criaremos a nossa database com a MongoDB, que utiliza o modelo NoSQL de Document e oferece uma opção gratuita adequada para a nossa aplicação. Cada usuário do app será um objeto como o exemplificado acima, com propriedades para e-mail, senha etc.

2. Uma ponte entre a aplicação e a database

Ótimo! Sabemos que precisamos de um banco de dados. E daí? Como eu boto as coisa lá?

Para estabelecer comunicação entre a nossa aplicação e a database, poderíamos escrever as queries com o próprio cliente da Mongo. Mas utilizaremos uma alternativa para facilitar o nosso trabalho. É aqui que entra o Prisma.

Prisma é um ORM (Object Relational Mapper), isto é, uma ferramenta que fará, por trás dos panos, o trabalho de construir, acessar e manipular os dados da database. É mais ou menos o que o React é para o Javascript em relação à manipulação do DOM. Com o Prisma, poderemos escrever códigos mais declarativos e menos imperativos. Ele não funciona apenas com a MongoDB, porque a função do Prisma é justamente abstrair as especificidades de cada database e fazê-las funcionar com a própria sintaxe.

Ele desempenhará, na nossa aplicação, duas funções:

  • Declaração dos esquemas dos documentos, para definir, por exemplo, que o Document de usuário será composto de um e-mail (string), uma senha (string) e uma lista de tarefas (array de strings). Pense em um schema como uma blueprint do Document.

  • Servirá como cliente para manipular a database, disponibilizando funções para facilmente alterar, criar e remover dados do banco.

3. Um meio de verificar se o usuário está logado e se pode acessar as informações da própria conta.

Agora, só precisamos lidar com a autenticação do usuário para proteger as nossas páginas e obter os dados do usuário logado.

Nós utilizaremos uma lib chamada NextAuth para este serviço. O NextAuth oferece meios de suprir as duas necessidades acima com funções que nos ajudam a registrar um novo usuário e a verificar o status de autenticação. Além disso, o NextAuth possui suporte para o Prisma, o que significa que ele usará o cliente e os esquemas do Prisma para essas tarefas.

Começando os trabalhos

Crie o seu projeto Next. Eu utilizei a versão 13.2.4 para escrever este artigo. Vamos instalar as dependências fundamentais do projeto.

Instalando o Prisma

Aqui vai uma dica: instale a extensão do Prisma para o VSCode se quiser dicas de sintaxe e linting no seu código.

  1. Começamos instalando as dependências necessárias para rodar o Prisma com npm i -D prisma. “-D” porque nós a instalaremos apenas como uma dependência de desenvolvimento.

  2. Depois, iniciaremos o prisma com npx prisma init. Isto criará uma pasta chamada prisma com um arquivo chamado schema.prisma na raiz do app.

  3. Por fim, instalaremos o pacote do PrismaClient com npm i @prisma/client.

No schema.prisma, modificaremos o provider para provider = "mongodb". O arquivo deve estar assim:

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mongodb"
  url      = env("DATABASE_URL")
}
Enter fullscreen mode Exit fullscreen mode

Agora, criaremos uma pasta chamada lib na pasta raiz do projeto. Em lib, colocaremos alguns códigos reutilizáveis em diversos lugares na aplicação. O primeiro deles será a criação de uma instância do PrismaClient. Como usaremos o Prisma várias vezes, é útil deixar o código pronto para importá-lo conforme as nossas necessidades.

Para o Next especificamente há um adendo. Cada instância do Prisma é uma nova conexão com o servidor. Por conta do modo como funciona o hot reload do Next, precisamos definir essa instância em uma variável global para evitar que, durante o desenvolvimento, novas instâncias sejam criadas a cada compilação e gerem um erro.

O snippet abaixo serve justamente para isso: se estivermos em um abiente de desenvolvimento, utilizaremos uma instância global do Prisma. É uma solução que encontrei nesta issue levantada no repositório do Prisma no Github.

Deste modo, em um novo arquivo chamado prismadb.js, na pasta lib, inserimos:

import { PrismaClient } from "@prisma/client";

let prismadb;
if (process.env.NODE_ENV === "production") {
 prismadb = new PrismaClient();
} else {
 if (!global.prismadb) {
  global.prismadb = new PrismaClient();
 }
 prismadb = global.prismadb;
}
export default prismadb;
Enter fullscreen mode Exit fullscreen mode

A partir de agora, utilizaremos o PrismaClient com a variável prismadb.

Criando a database com MongoDB

Vá até o site da MongoDB e crie uma conta. Uma vez logado, procure pela opção de criar database. Escolha a opção free. Crie um usuário e senha para acessar a database, selecione a opção de conexão de ambiente local e insira o seu IP.

Na próxima página, você deve procurar pelo botão “connect”. Escolha a opção de conexão pelo VSCode. O link providenciado será utilizado para que o Prisma se conecte à database.

Quando instalamos o Prisma, ele deve ter criado um arquivo .env no diretório raiz da aplicação. Se não criou, você mesmo pode criar. O propósito deste arquivo é possibilitar declaração de variáveis disponíveis apenas no servidor, as chamadas variáveis de ambiente. Perceba que o link gerado possui o seu usuário e que você deve preencher com a sua senha. Não queremos mostrar este link ao cliente (browser), por isso nós os colocamos no .env, que ficará assim:

DATABASE_URL="<link_disponibilizado>"
Enter fullscreen mode Exit fullscreen mode

Para acessar esta variável na aplicação, basta colocarmos, no lugar do valor, process.env.DATABASE_URL. No momento da compilação ou do build do projeto, o Next se encarregará de substituir a variável pelo que foi definido no .env.

Durante o desenvolvimento, faremos este processo para todas as variáveis de ambiente. Na produção, após o deploy, lembre-se de que você deve configurar todas as variáveis no seu serviço de hosting. Normalmente é fácil localizar esta opção.

Ah! Provavelmente o seu projeto já possui isso, mas é bom checar. No arquivo .gitignore, verifique se .envestá declarado, para evitar que as suas variáveis sejam guardadas no Github.

Criando os models

Os models serão a estrutura das entidades que queremos guardar na database. Definiremos aqui que um usuário, por exemplo, tem e-mail, senha, lista de tarefas etc. O que faremos é definir os esquemas da database.

Esquemas, modelos, qual a diferença?

Basicamente, esquemas são a estrutura na database, e modelos a estrutura na aplicação. Porque o Prisma cria os esquemas segundo os modelos definidos na aplicação, criar um o modelo é criar o esquema.

Os modelos serão criados no arquivo schema.prisma, criado pelo Prisma na raiz do projeto. Certifique-se de que o seu arquivo esteja assim:

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mongodb"
  url      = env("DATABASE_URL")
}
// Os modelos vão aqui.
Enter fullscreen mode Exit fullscreen mode

User model

Começamos criando um model para User. Tenha em mente que o definido aqui se traduzirá em um objeto BSON como o visto acima, no exemplo das guitarras.

model User {

}
Enter fullscreen mode Exit fullscreen mode

A primeira coisa que faremos é definir um ID para identificar usuários únicos. O Prisma gerará automaticamente um ID com o seguinte código, que poderá ser reutilizado em todos os modelos:

id String @id @default(auto()) @map("_id") @db.ObjectId
Enter fullscreen mode Exit fullscreen mode

Note que a definição do modelo é tipada. O Prisma utiliza Typescript para garantir a precisão dos dados inseridos. Se tentássemos registrar um id como número, obteríamos um erro.

Vamos delinear o código acima.

  • id: é o nome da propriedade.

  • String: o tipo da propriedade.

  • @id: é um atributo do Prisma que define esta propriedade como chave primária do documento. Chaves primárias são identificadores únicos para uma entidade. No nosso caso, estamos dizendo que cada User terá o id como seu identificador único. Podemos definir mais de uma propriedade como única, mas apenas uma pode ser a chave primária.

  • @default(auto()): é um atributo do Prisma que gera com esta função auto() um id único como valor padrão para esta propriedade.

  • @map(”_id”): por este atributo o Prisma está indicando que o campo “id” deverá gerar, na MongoDB, um campo “_id”, porque “_id” é o nome padrão da chave primária na MongoDB.

  • @db.ObjectId: na MongoDB, o id possui um tipo “ObjectId”. Este atributo apenas diz que o campo “_id” usará o tipo específico “ObjectId”.

Ufa! O lado bom é que só escreveremos essa linha uma vez. Todos os models poderão usar o mesmo modo de geração para um id único.

Agora temos a liberdade de decidir outras propriedades para o nosso user. Quero um nome, um e-mail, uma foto de perfil, uma lista de tarefas… vamos adicioná-las:

model User {
  id String @id @default(auto()) @map("_id") @db.ObjectId
  name String? 
  email String? @unique
  image String?
  hashedPassword String?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  todoList String[] 
  doneList String[]
  accounts Account[]
}
Enter fullscreen mode Exit fullscreen mode

Definir um tipo com “?” ao final significa que aquele campo não é obrigatório. Pode parecer que qualquer usuário registrado deve ter ao menos e-mail e senha, mas mais à frente também disponibilizaremos registro por OAuth (login através do Google, Github, Facebook etc.). Nesses casos, o usuário não precisa preencher esses campos.

Você também deve ter percebido que inserimos um campo “accounts”, tipado como array de Account, no nosso modelo de User. O que fizemos foi referenciar o modelo Account, que ainda não criamos, mas que servirá para guardar os dados de quem se registrar por OAuth. É uma array porque podemos, se quiser, permitir que um único usuário logue por mais de um método.

Account model

Este modelo cuidará dos casos de autenticação OAuth. Não é preciso se preocupar com os detalhes aqui. O código abaixo é fornecido pela documentação do NextAuth para utilizar a library com o Prisma.

model Account {
    id String @id @default(auto()) @map("_id") @db.ObjectId
    userId String @db.ObjectId
    type String
    provider String
    providerAccountId String
    refresh_token String? @db.String
    access_token String? @db.String
    expires_at Int? 
    token_type String?
    scope String?
    id_token String? @db.String
    session_state String?

    user User @relation(fields: [userId], references: [id], onDelete: Cascade) 

    @@unique([provider, providerAccountId])
}
Enter fullscreen mode Exit fullscreen mode

O único ponto que merece comentário é a relação que criamos entre User e Account. Perceba como a linha user cria esta relação. Definimos que user é um dado do tipo User. Quem é este user? Para obter a resposta, definimos um campo (userId) no próprio modelo Account que referenciará outro campo (id) no modelo User. userIddeverá ter o mesmo valor que id no modelo User. Assim, o Prisma sabe que useré um objeto do tipo User cujo idé igual a userId.

A opção onDeleteestá marcada como Cascade, o que significa que, se deletarmos uma Account, deletaremos o User.

Terminamos de definir os modelos! Para finalizar esta estapa, execute npx prisma db push, e os nossos modelos já estarão na MongoDB. Sugiro entrar na database e verificar se existem duas novas Collections com os nomes User e Account.

Configurações do NextAuth

Começamos instalando o Next Auth com npm i next-auth.

Vamos instalar também o Prisma Adapter do NextAuth com npm i @next-auth/prisma-adapter

Toda a “máquina” do NextAuth é uma API criada com o próprio Next. Os passos a seguir são exigidos pela documentação do NextAuth.

Crie, dentro da pasta “api” do Next, uma pasta chamada “auth”. Dentro de auth, crie um arquivo chamado [...nextauth].js. Este é o arquivo de configuração do NextAuth. Nele definiremos as configurações globais e os providers (meios de registro).

A base deste arquivo será assim (não se esqueça dos imports):

import NextAuth from "next-auth"
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import prismadb from "@/lib/prismadb";

export const authOptions = {
 adapter: PrismaAdapter(prismadb),
  providers: [],
}

export default NextAuth(authOptions)
Enter fullscreen mode Exit fullscreen mode

Apenas uma nota: os meus imports que começam com ‘@’, como o do prismadb, são custom paths adicionados no jsconfig.json. Isto é opcional. Importe os seus arquivos conforme as rotas da sua aplicação.

O primeiro provider que adicionaremos será Credentials, isto é, autenticação por credenciais (usuário e senha).

  1. Importe Credentials: import Credentials from "next-auth/providers/credentials" ;

  2. Adicione Credentials como um provider.

providers: [
      Credentials({
       id: "credentials",
       name: "credentials",
       credentials: {
        email: {
         label: "E-mail",
         type: "text",
        },
        password: {
         label: "Senha",
         type: "password",
        },
       },
Enter fullscreen mode Exit fullscreen mode

Os campos id e name servirão para identificar o provider quando fizermos uma request para esta API. Mais tarde, diremos “tente logar com estes dados utilizando ‘credentials’. Os campos labele type são utilizados pelo NextAuth para criar um formulário padrão de login. Nós vamos criar um formulário próprio, mas é bom fazer essa configuração por segurança.

Lógica de login

O que faremos agora é definir a lógica de login com credenciais. Criaremos, neste mesmo objeto de configuração de Credentials, uma função assíncrona do NextAuth chamada authorize() para encontrar o usuário na database.

Como o encontraremos? Utilizando o PrismaClient, cuja instância criamos acima. O PrismaClient possui um método chamado findUnique. Com ele, dizemos “onde” procurar (where) e o que procurar, e então ele retorna o objeto encontrado:

const user = await prismadb.user.findUnique({
    where: {
        email: credentials.email,
    },
});
Enter fullscreen mode Exit fullscreen mode

A função auhtorize() é basicamente um handler de API, ao qual enviaremos os dados de login (email e senha) e do qual obteremos uma resposta, que será o usuário.

Você notará que estamos utilizando uma função chamada compare() para verificar a senha do usuário ao declarar a variável isCorrectPassword. Quando criarmos a lógica de registro, usaremos um pacote chamado bcryptpara criptografar as senhas. A função compare() é uma função do próprio bcryptpara fazer a validação com base na senha criptografada.

Assim, instale bcrypt com npm i bcrypt e importe compare: import { compare } from "bcrypt".

A função auhtorize() deverá ser adicionada como outro objeto de Credentials:

// ...
// type: "password",
//    },
//   },

async authorize(credentials) {
    try {

     //Lidamos com o caso de ausência das credenciais
     if (!credentials.email || !credentials.password) {
      throw new Error("É preciso preencher o usuário e a senha.");
     }

     //Busca um usuário na db
     const user = await prismadb.user.findUnique({
      where: {
       email: credentials.email,
      },
     });

     //Se não encontra, retorna um erro.
     if (!user) {
      throw new Error("O usuário não existe.");
     }

     //Se o usuário existe, mas não possui senha, significa que ele se registrou com outro método.
     if (!user.hashedPassword) {
      throw new Error(
       "Parece que você se registrou com um método diferente."
      );
     }

     // Verifica se o password enviado no login bate com o password criptografado na database.
     const isCorrectPassword = await compare(
      credentials.password,
      user.hashedPassword
     );

     if (!isCorrectPassword) {
      throw new Error("Senha incorreta.");
     }

     return user;
    } catch (error) {
     throw new Error(error.message);
    }
   },
Enter fullscreen mode Exit fullscreen mode

Você pode estar se perguntando: “ok, mas como eu enviarei essas informações? De onde elas sairão?”. O próprio NextAuth oferece uma função para enviar os dados de login chamada signIn. Nós a criaremos mais tarde, e passaremos credentials e os dados de login como argumentos para que a lógica criada acima seja utilizada.

Depois do snippet acima, falta definirmos a estratégia para manter a sessão do usuário. Nós utilizaremos JWT, que significa JSON Web Token. Quando o usuário logar, a aplicação criará este token como um cookie no browser. O NextAuth lerá esse cookie a cada acesso e decidirá se a sessão é válida ou não.

Como tudo o que deve permanecer em segredo, definiremos a chave de geração do JWT nas variáveis de ambiente. Você decide o que será esse segredo. Uma boa sugestão é bater a cabeça no teclado e gerar uma chave segura, ou utilizar outros geradores confiáveis.

// ...
// throw new Error(error.message);
//    }
//   },

session: {
  strategy: "jwt",
 },
 jwt: {
  secret: process.env.NEXTAUTH_JWT_SECRET,
 },
 secret: process.env.NEXTAUTH_SECRET,
Enter fullscreen mode Exit fullscreen mode

E então iremos ao arquivo .env e declararemos as chaves criadas.

NEXTAUTH_JWT_SECRET="c56C+XzTJicxQZBdP3bViDf+ARmHJpFYy7prOg3Ji/wdx9uIxdpMLaL+JYgOkU"
NEXTAUTH_SECRET="h393jmE)_-o3mopdmnh#*4jh0m3JSlasl,*5nksp08nmjls@,lsjdej3pa9#çsj932"
Enter fullscreen mode Exit fullscreen mode

Por fim, vamos definir uma página padrão para onde o usuário será redirecionado caso tente acessar uma rota protegida, ou fazer o login e o servidor não validar a sessão. Você pode definir a página que quiser aqui (/signin, por exemplo):

//...
// secret: process.env.NEXTAUTH_SECRET,
pages: {
    signIn: "/signin",
},
Enter fullscreen mode Exit fullscreen mode

Ah! Se quiser depurar o seu código de forma que os logs apareçam no prompt local, pode adicionar esta propriedade também, depois de pages:

debug: process.env.NODE_ENV === "development",
Enter fullscreen mode Exit fullscreen mode

O nosso [...nextauth].js ficará assim (por enquanto):

import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { PrismaAdapter } from "@next-auth/prisma-adapter";

import { compare } from "bcrypt";
import prismadb from "@/lib/prismadb";

export const authOptions = {
 adapter: PrismaAdapter(prismadb),
 providers: [
  Credentials({
   id: "credentials",
   name: "credentials",
   credentials: {
    email: {
     label: "E-mail",
     type: "text",
    },
    password: {
     label: "Senha",
     type: "password",
    },
   },
   async authorize(credentials) {
    try {
     if (!credentials.email || !credentials.password) {
      throw new Error("É preciso preencher o usuário e a senha.");
     }

     const user = await prismadb.user.findUnique({
      where: {
       email: credentials.email,
      },
     });

     if (!user) {
      throw new Error("O usuário não existe.");
     }

     if (!user.hashedPassword) {
      throw new Error(
       "Parece que você se registrou com um método diferente."
      );
     }

     const isCorrectPassword = await compare(
      credentials.password,
      user.hashedPassword
     );

     if (!isCorrectPassword) {
      throw new Error("Senha incorreta.");
     }

     return user;
    } catch (error) {
     throw new Error(error.message);
    }
   },
  }),
 ],

 pages: {
  signIn: "/signin",

 },

 debug: process.env.NODE_ENV === "development",

 session: {
  strategy: "jwt",
 },
 jwt: {
  secret: process.env.NEXTAUTH_JWT_SECRET,
 },
 secret: process.env.NEXTAUTH_SECRET,
};
export default NextAuth(authOptions);
Enter fullscreen mode Exit fullscreen mode

Lógica de registro

Para registrar um usuário, vamos criar uma API na nossa aplicação. Se você não sabe criar APIs com Next, saiba que é como criar APIs com Node, com o adendo de que as rotas e controles são definidos no mesmo arquivo dentro da pasta API. Se você não sabe criar APIs com Node, sugiro que dê uma olhada no assunto antes de continuar.

Na pasta API, então, criaremos uma rota para o registro com um arquivo chamado register.js. Utilizaremos o cliente do Prisma para verificar a existência do usuário e registrá-lo, caso ele não exista, com o método create, que recebe um objeto data{} como argumento (os nossos dados):

const user = await prismadb.user.create({
 data: {
  name,
  email,
  hashedPassword,
 },
});
Enter fullscreen mode Exit fullscreen mode

Faremos uso, também, do bcrypt mencionado acima, para enviar a senha já criptografada à database.

Esta criptografia será feita com a função hash do pacote, que recebe uma string e um número como argumentos. A string é o que será criptografado, e o número a quantidade de vezes que a função será executada sobre essa string para criptografá-la. Valores maiores podem significar maior segurança, mas também significam mais recursos computacionais utilizados. O valor 12 nos dá um bom equilíbrio entre segurança e performance.

Nesta parte, devemos decidir o que será registrado. Se você fez como eu e adicionou apenas e-mail, senha e nome como dados relevantes para o modelo User, então queremos registrar aqui apenas estes campos.

No arquivo register.js, portanto, criaremos a seguinte API:

import bcrypt from "bcrypt";
import prismadb from "@lib/prismadb";

export default async function handler(req, res) {
 // Se o método não for POST, retorna 405 (método não permitido).
 if (req.method !== "POST") {
  return res.status(405).end();
 }

 try {
 // Desestrutura os dados enviados no body da requisição
  const { name, email, password } = req.body;

 // Se, por algum acaso, a validação do formulário não funcionar, a API garante que pelo menos o Nome, a Senha e o Email estão preenchidos para o registro.
  if (!name || !password || !email) {
   return res.status(401).json({ error: "É preciso preencher todos os campos." });
  }

 // findUnique seleciona, dentro da collection User, o usuário cujo e-mail seja igual ao e-mail passado na requisição. Definir apenas "email" é uma abreviação de "email: email".
  const existingUser = await prismadb.user.findUnique({
   where: {
    email,
   },
  });

  if (existingUser) {
   return res.status(422).json({ error: "Este e-mail já foi cadastrado." });
  }

 // Criptografa a senha
  const hashedPassword = await bcrypt.hash(password, 12);

 // Cria o usuário na database
  const user = await prismadb.user.create({
   data: {
    name,
    email,
    hashedPassword,
   },
  });

  return res.status(200).json(user);
 } catch (error) {
  return res.status(400).json({ error: "Algo deu errado :(" });
 }
}
Enter fullscreen mode Exit fullscreen mode

Criando o formulário

Já configuramos o login e o registro, mas ainda não proporcionamos um meio para que o usuário realize essas ações.

Como o propósito deste artigo é ensiná-lo a manejar o login e o registro de dados em databases, não me preocuparei em explicar os detalhes de criação de formulários com React, como funciona o controle de states etc., mas apontarei o que for relevante para a sua criação. Eu também providenciei o código completo de um formulário abaixo para você ter uma base.

A primeira coisa de que precisaremos é um state para definir se o formulário é de registro ou login. Você pode fazer um formulário para cada situação. Eu prefiro criar um só e mudar este state para definir qual das funções será chamada:

const [formType, setFormType] = useState("login");
Enter fullscreen mode Exit fullscreen mode

O essencial mesmo é um state para armazenar os valores dos inputs (se você está seguindo o artigo passo a passo, bastam esses campos):

const [formData, setFormData] = useState({
    name: "",
    email: "",
    password: "",
});
Enter fullscreen mode Exit fullscreen mode

E uma função para onChange de cada input, de forma que os campos preenchidos tenham o mesmo valor de formData.

 function handleChange(e) {
  setFormData((prev) => {
   return { ...prev, [e.target.id]: e.target.value };
  });
 }
Enter fullscreen mode Exit fullscreen mode

Por fim, um botão para enviar os dados às funções de registro e login (que criaremos a seguir):

 function handleSubmit(e) {
  // Não se esqueça de definir este preventDefault
  // para evitar o reload da página quando o usuário clicar no botão
  e.preventDefault();
  if (formType === "register") {
   register();
  } else if (formType === "login") {
   login();
  }
 }
Enter fullscreen mode Exit fullscreen mode

Se quiser, pode criar desde já um botão que registrará o usuário com o Google e outro com o Github. Nós lidaremos com essas opções mais à frente.

Eis um formulário que criei, se você quiser uma representação visual:

O código completo ficaria mais ou menos assim (sem estilos):

export default function Form() {

 const [warning, setWarning] = useState("warning");

 // State que define o tipo do formulário
 const [formType, setFormType] = useState("login");

 // State que guarda os valores dos inputs
 const [formData, setFormData] = useState({
  name: "",
  email: "",
  password: "",
 });

 function handleChangeFormType(type) {
  setWarning("warning");
  setFormType(type);
 }

 // Função que muda o state para guardar os inputs
 function handleChange(e) {
  setFormData((prev) => {
   return { ...prev, [e.target.id]: e.target.value };
  });
 }

 // Função que decide a função a ser chamada
 function handleSubmit(e) {
  e.preventDefault();
  setIsLoading(true);
  if (formType === "register") {
   register();
  } else if (formType === "login") {
   login();
  }
 }

 // JSX do formulário
 return (
  <>
   <div>

    <div>
     <button onClick={() => handleChangeFormType("login")}>
      Login
     </button>
     <button onClick={() => handleChangeFormType("register")}>
      Cadastro
     </button>
    </div>
    <form onSubmit={handleSubmit}>
     {formType === "register" && (
      <input
       type="text"
       placeholder="Nome"
       id="name"
       onChange={handleChange}
       required
      />
     )}
     <input
      type="email"
      placeholder="E-mail"
      id="email"
      onChange={handleChange}
      required
     />
     <input
      type="password"
      placeholder="Senha"
      id="password"
      onChange={handleChange}
      required
     />
     <p>
      {warning}
     </p>
     <button type="submit">
      {formType === "login" ? "Login" : "Cadastro"}
     </button>
    </form>
    <div>
     <button onClick={() => handleOAuth("google")}>
      <FcGoogle />
     </button>
     <button onClick={() => handleOAuth("github")}>
      <IoLogoGithub />
     </button>
    </div>
    <p>
     * Esta é uma aplicação de estudos. A senha é criptografada. Todos os dados
     podem ser deletados sem aviso prévio.
    </p>
   </div>
  </>
 );
}
Enter fullscreen mode Exit fullscreen mode

Função de registro

Vamos criar a função de registro por credenciais neste mesmo componente do formulário. Esta função será assíncrona, mais especificamente uma request do tipo POST feita à API register.js que criamos anteriormente.

Nós a declararemos dentro de um useCallBack porque ela receberá variáveis do formulário, e só queremos que a função seja recriada se o usuário escrever algo nos inputs.

A base dela será assim:

const register = useCallBack(async()=> {


}, [formData])
Enter fullscreen mode Exit fullscreen mode

Para fazer a request eu utilizarei Axios, que é uma library criada para facilitar manejo de APIs em Javascript. Instalá-lo não é necessário e você pode usar a função fetch() nativa sem problemas. Se quiser seguir os mesmos passos que eu, instale npm i axios e o importe no componente com import axios from "axios".

O que faremos, então, é uma simples request do tipo POST para a nossa API de registro. Lá nós dissemos que o body da request conteria name, password e email. Portanto, enviaremos estes campos no body da nossa request. Como o meu state formData contém esses campos já em um objeto ( {name: "nome", email: "email", password: "senha"}), posso enviá-lo diretamente.

const register = useCallback(async () => {

try {
   await axios.post("/api/register", formData);
   login();
  } catch (error) {
   console.log(error);
   setWarning(error.response.data.error);
  }
 }, [formData, login]);
Enter fullscreen mode Exit fullscreen mode

Note que dentro do bloco catch estou alterando um state do componente para alertar ao usuário a ocorrência de algum erro. O objeto error.response.data.error, é como acesso o texto do erro retornado pelo Axios, e o texto deste erro definimos lá na API. Assim, se o usuário já existe, por exemplo, o formulário mostrará a mensagem "Este usuário já existe.”.

Você também deve ter notado que chamei a função login() logo após a request. Fiz isto para que o usuário seja automaticamente autenticado caso o registro ocorra. Ainda não criamos esta função, mas já é possível testar o nosso registro. Remova a call de login() por enquanto e tente se registrar pela sua aplicação, verificando se um novo Document de usuário aparece na MongoDB. Se tudo estiver funcionando, vamos ao próximo passo.

Função de login

Para criar a função de login, utilizaremos uma função fornecida pelo NextAuth chamada signIn().

Começamos importando-a: import {signIn} from "next-auth/react"

O método signIn() recebe dois argumentos: o id do provider e um objeto com os dados de login. Lembre-se que nós definimos no arquivo de configuração do NextAuth “credentials” como o id do provider de credenciais. Quanto ao segundo argumento, passaremos email e password, retirados do nosso state que guarda os valores dos inputs.

Também passaremos a opção redirect: false para evitar que o usuário seja redirecionado na tentativa de login. Vamos nós mesmos lidar com esse redirecionamento usando o roteamento do Next.

const login = useCallback(async () => {
  try {
   const res = await signIn("credentials", {
    email: formData.email.trim(),
    password: formData.password.trim(),
    redirect: false,
   });

   if (res.ok) {
    router.push("/user");
   } else {
    throw new Error(res.error);
   }

  } catch (error) {
   setWarning(error.message);
  }
 }, [formData, routerl]);
Enter fullscreen mode Exit fullscreen mode

Deixe-me explicar o snippet acima. A função signIn() retorna um objeto. Se este objeto contiver a propriedade ok, significando que a request foi bem sucedida, vamos redirecioná-lo para a página de usuário na nossa aplicação, que eu criei no path “/user”. Se a resposta não for ok, jogará um erro que será lido pelo bloco catch e mudará o state warning que eu criei para mostrar eventuais erros de login.

Pronto! Agora o usuário já pode logar! Crie a página para a qual o usuário será redirecionado na sua aplicação e teste!

Quero fazer apenas um adendo aqui. Certifique-se de ter declarado a função login antes de register. Como estamos chamando a função de login dentro da função de registro, ela precisa ser inicializada antes.

//primeiro login
const login = useCallback...

//depois register
const register = useCallback...
Enter fullscreen mode Exit fullscreen mode

Adicionando outros tipos de autenticação

  • Esta parte é opcional. Se você não pretende adicionar login com métodos diferentes, pode pular para a próxima seção.

Nós fizemos a parte mais difícil, que é criar autenticação com credenciais. Vamos adicionar alternativas OAuth para o usuário, como Google e Github. OAuth significa Open Authorization. É uma forma de reutilizar os dados já registrados em uma aplicação terceirizada para registrar o usuário na sua própria aplicação. Nestes casos, a senha utilizada na aplicação terceirizada não é exposta.

Começamos incluindo lá no [...nextauth].js os providers do Github e do Google.

Importamos os providers:

import GithubProvider from "next-auth/providers/github";
import GoogleProvider from "next-auth/providers/google";
Enter fullscreen mode Exit fullscreen mode

E, dentro da array de providers, onde definimos Credentials, definiremos os novos providers:

providers: [
  GithubProvider({
   clientId: process.env.GITHUB_ID || "",
   clientSecret: process.env.GITHUB_SECRET || "",
  }),
  GoogleProvider({
   clientId: process.env.GOOGLE_ID || "",
   clientSecret: process.env.GOOGLE_CLIENT_SECRET || "",
  }),
 // Credentials({
 // ...
Enter fullscreen mode Exit fullscreen mode

Só isso não basta. Precisamos agora pedir autorização desses providers para utilizar os seus serviços. Com a autorização, obteremos as variáveis de ambiente indicadas acima para clientId e clientSecret.

Github

Primeiro, devemos criar uma chave de autenticação no Github. No site do Github, encontramos esta opção em Settings > Developer Settings > OAuth Apps. Preenchemos o nome da nossa aplicação (que aparecerá ao usuário). Os campos de Homepage URL e Authorization callback URL podemos preencher com “http://localhost:3000/” (ou a URL local que você está utilizando) já que estamos em um ambiente de desenvolvimento.

Guardamos, então, as chaves de ID e o Secret do cliente nas nossas variáveis de ambiente com os nomes de variável definidos nas configurações do NextAuth (GITHUB_ID e GITHUB_SECRET, no caso do nosso exemplo).

Google

A única diferença será o modo de criação das chaves. Para o Google, entramos na plataforma da Google Cloud e criamos um Novo Projeto. Certificamo-nos de que este projeto está selecionado, e procuramos pela opção APIs e serviços (pode-se utilizar o campo de pesquisa do site). No menu lateral, selecionamos Tela de permissão OAuth. Escolhemos uso Externo e depois preenchemos o formulário com as informações da nossa aplicação.

Como estamos em um ambiente de desenvolvimento, na parte de Escopos e Usuários de teste não precisamos preencher nada.

Agora, no menu lateral selecionamos Credenciais e clicamos em Criar Credenciais > ID do Cliente OAuth. Selecionamos Aplicação Web e definimos o nome da aplicação.

Agora, o único campo que precisamos preencher será Adicionar URI. Nele, devemos adicionar este link, utilizado pelo NextAuth:

http://localhost:3000/api/auth/callback/google

Lembrando sempre que utilizamos “localhost:3000” por estarmos em um ambiente local de desenvolvimento. Se você fizer deploy do seu site em algum momento, será preciso alterar todas as configurações dos providers e das variáveis de ambiente para a URL de produção.

Lidando com o login por OAuth

Vamos criar a última função do nosso formulário para lidar com o caso autenticação por OAuth.

Se você ainda não criou um botão para o Google, Github ou outros providers no seu formulário, a hora é agora.

Será uma função simples função assíncrona que chamará signIn com o id do provider como argumento. Também passaremos um objeto como segundo argumento, com a propriedade callBackUrl definindo a URL de redirecionamento caso a autorização seja um sucesso.

Um bom modo de criar esta função é passar provider como parâmetro e alterar o argumento na call da função conforme o provider, deste modo:

async function handleOAuth(provider) {
  try {
   await signIn(provider, { callbackUrl: "/user" });
  } catch (error) {
   console.log(error);
  } 
 }
Enter fullscreen mode Exit fullscreen mode

Segundo a documentação do NextAuth, o id do provider do Google é google, e do Github é github. Para outros casos, cheque a documentação.

Finalmente, para chamar esta função definimos um onClick nos nossos botões, por exemplo: onClick={() => handleOAuth("google")}.

Pronto, agora o usuário poderá logar com Credenciais, Github e Google na nossa aplicação!

Protegendo as nossas rotas

Precisamos nos certificar de que apenas usuários autenticados consigam acessar certas rotas. Para isso, vamos utilizar o getServerSidePropsdo Next e uma função chamada getServerSession do NextAuth.

Como sabemos, o Next fornece a possibilidade de se executar funções no servidor antes da renderização da página com getServerSideProps. Não queremos passar props para o componente, mas utilizaremos a função para verificar se o cliente possui uma sessão ativa antes de mostrar qualquer conteúdo. Se não houver, o usuário será redirecionado para a nossa página de registro ou login.

Como verificar se há uma sessão ativa? Obteremos a resposta através de getServerSession. Esta função retorna um objeto com algumas informações do usuário da sessão atual, como nome e e-mail. Precisamos passar a ela req e res como argumentos, que nós acessaremos a partir do context do getServerSideProps. O terceiro argumento são as opções do NextAuth, que foram exportadas no arquivo […nextauth].js.

Nas páginas que queremos proteger (aquelas em que o usuário deve estar logado para visualizar), vamos fazer a verificação no lado do servidor. A função a seguir deve ser declarada após os imports e antes do componente da página (como todo getServerSideProps):

import { getServerSession } from "next-auth";
import { authOptions } from "@/pages/api/auth/[...nextauth]";

export async function getServerSideProps(context) {
 const session = await getServerSession(context.req, context.res, authOptions);

 if (!session) {
  return {
   redirect: {
    destination: "/signin",
   },
  };
 }

 return {
  props: {},
 };
}
Enter fullscreen mode Exit fullscreen mode

Deste modo, se não houver session válida, o usuário será redirecionado para a página escolhida.

Obtendo os dados do usuário

Já temos uma lógica para registro e login, e já estabelecemos um meio de proteger as nossas rotas. O que precisamos, agora, é obter os dados do usuário atual para renderizá-los no cliente.

Esta parte pode ficar um pouco confusa, então preste atenção: para obter esses dados, vamos criar mais uma API. E que lógica ela precisa conter? Algo que nos permita selecionar o usuário atual na database. Poderíamos criar esta lógica diretamente na API, mas pense lá na frente: toda vez que quisermos alterar alguma informação sobre o usuário precisaremos selecioná-lo antes. Obter os dados? Selecione o usuário e retorne. Adicionar uma tarefa? Selecione o usuário e adicione.

Vamos, por isso, criar uma outra lib, que retornará o nosso usuário atual para utilizá-lo em todas as nossas APIs.

Depois, vamos criar uma API apenas para obter o usuário atual. Ela será nossa rota current.

Após estes passos, criaremos finalmente um meio para utilizar os dados obtidos no cliente.

Função para ser usada nas APIs

Na pasta lib vamos criar um arquivo chamado serverAuth.js e escrever uma função que retornará o usuário selecionado.

Nesta função, utilizaremos mais uma vez getServerSession do NextAuth para obter o e-mail do usuário. Os parâmetros req e res serão supridos porque chamaremos serverAuth apenas dentro das nossas APIs, que já possuem essas informações como parâmetros. A call da API poderá enviar req e res como argumentos para que o NextAuth cuide de obter os dados da sessão.

Obtida a sessão, temos o e-mail do usuário logado. Com este e-mail, utilizaremos findUnique do PrismaClient para selecionar o usuário da database. Se esse usuário for encontrado, vamos retorná-lo. A função ficará assim:

import prismadb from "./prismadb";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/pages/api/auth/[...nextauth]";

const serverAuth = async (req, res) => {

 //obtém dados da sessão
 const session = await getServerSession(req, res, authOptions);

 //se não há sessão, retorna um erro
 if (!session.user.email) {
  throw new Error("O usuário não está logado.");
 }

 // seleciona o usuário com o cliente do Prisma
 const currentUser = await prismadb.user.findUnique({
  where: {
   email: session.user.email,
  },
 });

 //se assegura de que um usuário foi selecionado
 if (!currentUser) {
  throw new Error("O usuário não existe.");
 }

 //finalmente, retorna o nosso usuário
 return { currentUser };
};

export default serverAuth;
Enter fullscreen mode Exit fullscreen mode

Criando a API

Na pasta api do Next, vamos criar uma nova rota com o nome current.js. Ela apenas retornará o currentUser de serverAuth, conforme as explicações acima.

export default async function handler(req, res) {
 if (req.method != "GET") {
  return res.status(405).end(); //incorrect request type
 }

 try {
  const { currentUser } = await serverAuth(req);
  return res.status(200).json(currentUser);
 } catch (error) {
  console.log(error);
  return res.status(400).end();
 }
}
Enter fullscreen mode Exit fullscreen mode

Utilizando useSWR e um custom hook

A lógica da API está criada. É hora de criar a lógica para obter os dados no cliente. Para isto, utilizaremos SWR (Stale-While-Revalidate), uma lib de data fetching criada pela Vercel (a mesma companhia por trás do Next) que nos permitirá utilizar os dados obtidos com facilidade.

Funciona da seguinte maneira: o hook useSWR faz uma request para uma API e armazena o resultado no cache do browser. A URL passada como argumento para o hook (a rota da nossa API, no caso) se torna uma ‘key’ daquela request compartilhada por toda a aplicação. Se este hook for utilizado com a mesma chave em outro local do app, os dados serão obtidos diretamente do cache e não a partir de outra request. É uma forma de compartilhar o usuário por toda a aplicação sem precisar usar contextos.

O hook possui muitas opções e é bastante otimizado. É possível definir, por exemplo, que a revalidação será feita em um determinado intervalo de tempo ou a cada mudança de tela no browser. Ele também fornece uma variável que indica o estado da request (isLoading) e uma função para modificar os dados manualmente, sem a necessidade de outra request (a função mutate), o que é muito útil para renderizar automaticamente dados modificados na database.

E por que estou falando em “custom hook”? Vamos criar um hook para utilizar useSWR. Pode parecer exagero, mas fazer isto economizará várias linhas quando quisermos acessar os dados do usuário na aplicação.

Criando o hook

Crie uma nova pasta chamada hooks. Dentro dela, crie um arquivo chamado useCurrentUser.js.

Um custom hook do React nada mais é que um componente que pode agrupar o resultado de outros hooks e funções em um único arquivo para ser usado em vários lugares da aplicação.

Vamos instalar o pacote do SWR com npm i swr, e importá-lo com import useSWR from "swr".

useSWR recebe como argumentos a URL da nossa API e uma função para fazer a request. A URL é a rota /api/current que criamos. A função é um fetch GET simples para a API, que nós definiremos com Axios. Importe, portanto, o Axios no seu arquivo.

O hook ficará assim:

import useSWR from "swr";
import axios from "axios";

const useCurrentUser = () => {
 const fetcher = (url) => axios.get(url).then((res) => res.data);

 const { data, error, isLoading, mutate } = useSWR("/api/current", fetcher);

 return {
  data,
  error,
  isLoading,
  mutate,
 };
};

export default useCurrentUser;
Enter fullscreen mode Exit fullscreen mode

Perceba que useSWR retorna data, error, isLoading e mutate, e nós estamos retornando esses valores no nosso próprio hook.

  • data: é o objeto do usuário na database. Este valor contém tudo o que o modelo User contém.

  • error: objeto que contém eventuais erros retornados da request.

  • isLoading: state que é true durante a request e false quando ela é finalizada.

  • mutate: função que permite a revalidação dos dados no cache e a sua manipulação.

Testando a request

É a hora da verdade. Suponho que você já tenha se registrado e esteja logado na aplicação. Vá até a página que você protegeu com getServerSideProps e importe useCurrentUser.

Chame o hook desta forma:

const { data: user, error, isLoading, mutate } = useCurrentUser()
Enter fullscreen mode Exit fullscreen mode

Ele retorna o objeto data, que é o nome padrão do useSWR. O que fizemos acima foi apenas renomear data para user, mas isto não é obrigatório.

Dê um log para verificar o que é o objeto user e renderize, em um HTML qualquer, o nome do usuário:

<p> Olá, {user?.name}!</p>
Enter fullscreen mode Exit fullscreen mode

Note que adicionamos uma condicional ? a user. A sintaxe acima é o equivalente a user && user.name, isto é, “se user for truthy, renderize user.name”. Isto garante que o Next não vai tentar renderizar uma variável indefinida.

Se você quer adicionar outra camada de segurança ou sinalizar visualmente ao usuário que os dados ainda não retornaram, pode renderizar seus componentes de acordo com o valor de isLoading e error:

export default function UserPage() {
const { data: user, error, isLoading, mutate } = useCurrentUser()

if (error) {
 return <p>Oops, algo deu errado!</p>
}

 return (
  <>
   {isLoading  ? <p>Carregando...</p> : <p> Olá, {user?.name}!</p>}
   <button onClick={() => signOut()}>Sair</button>
  </>
 );
}
Enter fullscreen mode Exit fullscreen mode

Adicionei no snippet acima um botão para deslogar. Ela utiliza a função signOut do NextAuth. Basta chamá-la para que a library cuide de remover a sessão atual.

Adicionando uma tarefa

Os fundamentos da aplicação estão construídos! O usuário já pode se registrar e logar, já protegemos nossas páginas de usuários não autenticados e já estamos renderizando os dados da database no cliente.

Falta apenas adicionar um método de manipulação dos dados. Criaremos, então, uma API para que o usuário possa adicionar uma tarefa na database (lembrando que estou utilizando uma lista de tarefas apenas como plano de fundo deste artigo).

Criando a API

Já temos tudo o que precisamos para incluir essa finalidade. Utilizaremos o cliente do Prisma para adicionar a tarefa.

Na pasta api, criaremos uma rota chamada todo.js (de to do, isto é, uma tarefa “para fazer”) e escreveremos o esqueleto do handler. Como estamos adicionando uma tarefa, vamos criar nossa lógica para o método POST:

import prismadb from "@/lib/prismadb";
import serverAuth from "@/lib/serverAuth";

export default async function handler(req, res) {

 try {
  if (req.method === 'POST') {

  }

 } catch (error) {
  console.log(error);
  return res.status(400).json("Oops! Algo deu errado.");
 }
}
Enter fullscreen mode Exit fullscreen mode

Dentro da condicional vamos obter o usuário atual com o serverAuth que criamos para utilizar em todas as APIs (lembre-se que serverAuth retorna o currentUser):

const { currentUser } = await serverAuth(*req*, *res*);
Enter fullscreen mode Exit fullscreen mode

Quando criarmos um formulário para adicionar a tarefa, passaremos uma tarefa (string) no body da request. Vamos obter essa tarefa desestruturando o body:

const { todo } = req.body;
Enter fullscreen mode Exit fullscreen mode

Vamos adicionar a tarefa na lista do usuário, utilizando o método update do Prisma, que nos permite adicionar diretamente um elemento em uma array com push:

const response = await prismadb.user.update({
   where: {
    email: currentUser.email,
   },
   data: {
    todoList: {
     push: todo,
    },
   },
  });

return res.status(200).json("Adicionado com sucesso.");
Enter fullscreen mode Exit fullscreen mode

Ficará assim:

import prismadb from "@/lib/prismadb";
import serverAuth from "@/lib/serverAuth";

export default async function handler(req, res) {

 try {
  const { todo } = req.body;

  if (req.method === 'POST') {
   const { currentUser } = await serverAuth(req, res);

   const response = await prismadb.user.update({
      where: {
       email: currentUser.email,
      },
      data: {
       todoList: {
        push: todo,
       },
      },
     });

   return res.status(200).json("Adicionado com sucesso.");
  }

 } catch (error) {
  console.log(error);
  return res.status(400).json("Oops! Algo deu errado.");
 }
}
Enter fullscreen mode Exit fullscreen mode

Lógica no cliente

Agora, vá até a sua página e crie uma função assíncrona para fazer uma request do tipo POST à rota (estou utilizando Axios):

async function addTodo(todo) {
  try {
   const res = await axios.post("/api/todo", {
    todo: todo,
   });
  } catch (error) {
   console.log(error.message);
  }
 }
Enter fullscreen mode Exit fullscreen mode

Depois, crie um input e um botão simples para que o usuário digite a tarefa e chame a função com o valor do input. Aproveite e faça uma ul para mostrar as tarefas adicionadas na database, utlizando o campo todoList de user, que já obtivemos através do hook useCurrentUser. Com base na página que criamos anteriormente, as adições a deixarão mais ou menos assim:

export default function UserPage() {
const { data: user, error, isLoading, mutate } = useCurrentUser()
const [todo, setTodo] = useState('');

async function addTodo(todo) {
  try {
   const res = await axios.post("/api/todo", {
    todo: todo,
   });
   console.log(res.data);
  } catch (error) {
   console.log(error.message);
  }
 }

if (error) {
 return <p>Oops, algo deu errado!</p>
}

 return (
  <>
   {isLoading  ? <p>Carregando...</p> : <p> Olá, {user?.name}!</p>}
   <button onClick={() => signOut()}>Sair</button>

   <input type="text" value={todo} onChange={(e) => setTodo(e.target.value)} />
   <button onClick={() => addTodo(todo)}>Adicionar tarefa</button>

   <ul>
     {user.todoList.map((todo, index) => (
      <li key={index}>{todo}</li>
     ))}
   </ul>


  </>
 );
}
Enter fullscreen mode Exit fullscreen mode

Eu adicionei a tarefa, mas ela não aparece na minha página!

A tarefa foi adicionada à database. Se você recarregar a página, verá que ela será renderizada. O problema é que estamos apenas atualizando a database sem avisar a aplicação.

Nós poderíamos criar um state apenas para armazenar a lista de tarefas, e incluir nesse state cada nova tarefa adicionada, mas o SWR nos fornece uma boa alternativa.

Como mencionado anteriormente, chamar mutate força o SWR a revalidar os dados no cache e re-renderizar o componente. Poderíamos, então, simplesmente chamar mutate() na função addTodo, logo após a request (ainda dentro de try, é claro).

No entanto, fazer isto pode comprometer a performance, pois uma nova request será feita cada vez que um dado for alterado. Graças à engenhosidade da Vercel, mutate pode receber os dados novos como argumento incluí-los no cache sem realizar outra request. Vamos atualizar a função addTodo:

async function addTodo(todo) {
  try {
   const newTodos = {...user, todoList: [...user.todoList, todo]}
   const res = await axios.post("/api/todo", {
    todo: todo,
   });
   mutate(newTodos, false)
   console.log(res.data);
  } catch (error) {
   console.log(error.message);
  }
 }
Enter fullscreen mode Exit fullscreen mode

Perceba que os dados que nós tinhamos compunham o objeto user. Logo, precisamos atualizar todo o objeto, declarando que todoList será a lista de tarefas antiga mais a nova tarefa. O segundo argumento de mutate, que definimos como false, é na verdade um objeto que guarda as opções da função, como a de revalidar os dados. Usar false é declarar que não queremos os dados revalidados, apenas adicionados ao cache.

Lembro apenas que este mutate é o retornado do useCurrentUser, que utiliza, por sua vez, o mutate do useSWR com a key ‘/api/current’. Se você quiser usar SWR para outras requests na sua aplicação, lembre-se de renomear o mutate de acordo com o caso, como fizemos com user, que antes era simplesmente data.

Removendo uma tarefa

Falta lidarmos apenas com um caso essencial. Vamos até a nossa API todo para criar a lógica de remoção de itens.

Diremos o seguinte: se o método for DELETE, selecionaremos a lista da database. Com a lista obtida, vamos filtrá-la para remover o item e, depois, substituir a lista antiga pela lista filtrada:

if (req.method === "DELETE") {
 const list = await prismadb.user.findUnique({
    where: {
     email: currentUser.email,
    },
    select: {
     todoList: true,
    },
   });

 const filteredList = list.todoList.filter(el => el != todo);

 const response = await prismadb.user.update({
    where: {
     email: currentUser.email,
    },

    data: {
     todoList: {
      set: filteredList,
     },
    },
   });

   res.status(200).json("Tarefa removida com sucesso.");

}
Enter fullscreen mode Exit fullscreen mode

Se você logar list verá que é um objeto que contém a lista como propriedade, por isso usamos list.todoList para manipular a lista com o método filter.

Obviamente filtrar uma array de strings não é um bom método de exclusão de itens, já que duas tarefas idênticas seriam removidas e o usuário poderia querer remover apenas uma delas. Poderíamos criar, ao invés de uma simples array de strings, uma array de objetos em que cada tarefa possui um ID, e filtrar este ID ao invés da própria “todo”. Mas o método atual serve ao propósito de demonstração do artigo.

O importante é entender que, para remover um item de uma array, selecionamos a lista, modificamos o necessário e recolocamos a nova lista no lugar.

Agora, para deletar a tarefa, criaremos uma função na aplicação que, ao ser chamada, faz uma request do tipo DELETE à API, passando a tarefa como argumento:

async function removeTodo(todo) {
  try {
   const filteredList = user.todoList.filter(el => el != todo);
   const newTodos = {...user, todoList: filteredList}
   const res = await axios.delete("/api/todo", {
    data: {todo: todo}
   });
   mutate(newTodos, false);
   console.log(res.data);
  } catch (error) {
   console.log(error.message);
  }
 }
Enter fullscreen mode Exit fullscreen mode

Mais uma vez a nossa função criará a lista atualizada e a colocará no cache para não precisar fazer outra request com SWR. Para chamar a função, criamos um botão e passamos todo como argumento:

<ul>
{user.todoList.map((todo, index) => (
<li key={index}>
<p>{todo}</p>
<button onClick={() => removeTodo(todo)}>Remover</button>
</li>
))}
</ul>
Enter fullscreen mode Exit fullscreen mode




Considerações finais

Pronto! É isso. Final anticlimático, né? Deixe-me melhorar.

Meus sinceros parabéns! A aplicação está criada e funcionando. Nosso usuário pode se registrar, logar, adicionar e remover tarefas. O que mais queremos? Talvez uma função para editar as tarefas? Outra para marcar tarefas como realizadas (movendo-as para a nossa doneList)? Deixar a página bonita para ninguém querer filtrar os olhos para fora da cabeça? Isso tudo deixo com você.

Espero que você tenha compreendido meu propósito, que não era criar uma aplicação específica do início ao fim, mas usar o tempo que outros tutorais gastam com aspectos individuais de cada projeto para explicar o que eles não explicam. O que construímos aqui foi um esqueleto, e o que tentei fazer foi capacitá-lo a colocar nele a pele que quiser.

Se você ficou com alguma dúvida, tem alguma sugestão ou feedback sobre o tutorial, por favor, não hesite em entrar em contato.

Um abraço! E happy coding :)

Top comments (0)