Next.js: Projeto Final: Autenticação & Configuração
Última atualização: 2026-08-26
Construir um projeto SaaS completo é como construir um edifício — primeiro, você usa andaimes para erguer a estrutura; depois, projeta os sistemas elétrico e hidráulico (o banco de dados); e, finalmente, instala as fechaduras das portas (o sistema de autenticação).
1. O Que Você Vai Aprender
- Inicializar um projeto de nível empresarial usando
create-next-app+ Tailwind + shadcn/ui - Prisma Schema: Cinco Modelos de Design (User / Organization / Project / Task / Comment) e Tipos de Enumeração
- Integração Auth.js v5: Solução de Autenticação Dupla Usando Credentials e Google OAuth
- Sistema de Código de Convite de Organização e Gerenciamento de Membros
- Proteção de Rotas com Middleware e Gerenciamento Centralizado de Sessões
- Seeding de Banco de Dados: Gerar 100 registros de exemplo
2. A História Real de um Engenheiro Full-Stack
(1) Ponto de Dor: O Início Caótico de Construir uma Plataforma SaaS do Zero
Alice é engenheira full-stack em uma startup de 50 pessoas. A empresa decidiu desenvolver uma plataforma interna de colaboração de projetos chamada "TaskFlow" para atender 10.000 usuários, cada um gerenciando mais de 200 tarefas. Alice tentou configurar a estrutura do projeto manualmente — mas o roteamento virou uma bagunça, as migrações de banco de dados falhavam frequentemente e todas as três soluções de autenticação que ela propôs foram rejeitadas. A equipe estava perdendo quatro horas por dia em configuração de ambiente e o projeto estava duas semanas atrasado.
(2) Solução Usando Scaffolding + Framework de Autenticação
Inicialização com um clique usando
create-next-app+ modelagem declarativa com Prisma ORM + Auth.js pronto para uso.
npx create-next-app@latest taskflow --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
npx prisma init --datasource-provider postgresql
npx auth add
Três linhas de código resolvem 80% do trabalho de inicialização.
(3) Resultados
| Dimensão | Configuração Manual | Solução TaskFlow |
|---|---|---|
| Tempo de Inicialização | 2 dias | 2 horas |
| Desenvolvimento da Solução de Autenticação | 1 semana (JWT próprio) | 30 minutos (integração Auth.js) |
| Migração de Banco de Dados | Scripts SQL Manuais | Prisma Migrate (Declarativo) |
| Diretrizes de Estrutura do Projeto | Cada equipe com seu estilo | App Router: Roteamento Convencional |
| Manutenibilidade | Baixa (sem type safety) | Alta (TypeScript + tipos Prisma) |
3. Inicialização e Configuração do Projeto
(1) Escolhendo a ferramenta de scaffolding create-next-app
graph LR
A[create-next-app] --> B[TypeScript]
A --> C[Tailwind CSS]
A --> D[ESLint]
A --> E[App Router]
A --> F[Diretório src/]
A --> G[Alias @/]
style A fill:#cce5ff
style B fill:#d4edda
style C fill:#d4edda
| Opção | Valor | Descrição |
|---|---|---|
| TypeScript | Sim | Type safety — obrigatório para produção |
| ESLint | Sim | Padrões de Codificação |
| Tailwind CSS | Sim | Funciona com shadcn/ui |
Diretório src/ |
Sim | Separado de app/ para uma estrutura clara |
| App Router | Sim | Padrão do Next.js 16 |
| alias de import | @/* |
caminho de import conciso |
▶ Exemplo: Inicializando um Projeto TaskFlow
Saída:
Diagrama do fluxo de dados e estrutura de componentes.
# 1. Criar um Projeto
npx create-next-app@latest taskflow --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
# 2. Ir para o Diretório
cd taskflow
# 3. Instalar shadcn/ui
npx shadcn@latest init -d
# 4. Adicionar Componentes Comuns do shadcn/ui
npx shadcn@latest add button card input label select dialog dropdown-menu avatar badge separator
# 5. Iniciar o servidor de desenvolvimento
npm run dev
✔ Projeto criado em: taskflow/
✔ shadcn/ui inicializado com sucesso
✔ Componente button instalado
✔ Componente card instalado
✔ Componente input instalado
✔ 7 componentes instalados no total
Saída:
✔ Projeto criado em: taskflow/
✔ shadcn/ui inicializado com sucesso
✔ Componente button instalado
✔ Componente card instalado
✔ Componente input instalado
✔ 7 componentes instalados no total
(2) Estrutura de Diretórios do Projeto
taskflow/
├── src/
│ ├── app/ # Páginas do App Router
│ │ ├── (auth)/ # Grupo de Rotas Relacionadas à Autenticação
│ │ ├── (dashboard)/ # Grupo de Rotas do Dashboard
│ │ ├── api/ # Rotas de API
│ │ ├── layout.tsx # Layout Raiz
│ │ └── page.tsx # Página Inicial
│ ├── components/ # Componentes Compartilhados
│ │ ├── ui/ # Componentes shadcn/ui
│ │ └── forms/ # Componentes de Formulário
│ ├── lib/ # Funções Utilitárias
│ │ ├── auth.ts # Configuração Auth.js
│ │ ├── db.ts # Prisma Client
│ │ └── utils.ts # Ferramentas de Uso Geral
│ ├── prisma/ # Prisma Schema + Migração
│ │ ├── schema.prisma
│ │ └── seed.ts
│ └── middleware.ts # Middleware de Proteção de Rotas
├── public/ # Recursos Estáticos
├── next.config.ts # Configuração Next.js
├── tailwind.config.ts # Configuração Tailwind
└── package.json
4. Design dos Cinco Modelos do Prisma Schema
(1) Diagrama de Relacionamento do Modelo de Dados
graph TB
U[User] -->|pertence a| O[Organization]
O -->|tem muitos| P[Project]
P -->|tem muitas| T[Task]
T -->|tem muitos| C[Comment]
U -->|atribuído a| T
U -->|criado por| C
U -->|criado por| P
style U fill:#cce5ff
style O fill:#d4edda
style P fill:#ffeeba
style T fill:#f8d7da
style C fill:#d6d8db
(2) Definindo Tipos de Enumeração
| Nome do Enum | Valor | Descrição |
|---|---|---|
Role |
OWNER / ADMIN / MEMBER / VIEWER |
Papéis Organizacionais |
ProjectStatus |
ACTIVE / ARCHIVED / COMPLETED |
Status do Projeto |
TaskStatus |
TODO / IN_PROGRESS / DONE |
Status da Tarefa |
TaskPriority |
LOW / MEDIUM / HIGH / URGENT |
Prioridade da Tarefa |
(3) Diagrama Prisma Completo
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum Role {
OWNER
ADMIN
MEMBER
VIEWER
}
enum ProjectStatus {
ACTIVE
ARCHIVED
COMPLETED
}
enum TaskStatus {
TODO
IN_PROGRESS
DONE
}
enum TaskPriority {
LOW
MEDIUM
HIGH
URGENT
}
model User {
id String @id @default(cuid())
name String?
email String @unique
emailVerified DateTime?
image String?
passwordHash String?
organizationId String?
organization Organization? @relation(fields: [organizationId], references: [id])
role Role @default(MEMBER)
projects Project[]
assignedTasks Task[] @relation("TaskAssignee")
comments Comment[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
accounts Account[]
sessions Session[]
}
model Organization {
id String @id @default(cuid())
name String
slug String @unique
inviteCode String @unique @default(cuid())
logo String?
users User[]
projects Project[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Project {
id String @id @default(cuid())
name String
description String?
status ProjectStatus @default(ACTIVE)
organizationId String
organization Organization @relation(fields: [organizationId], references: [id])
creatorId String
creator User @relation(fields: [creatorId], references: [id])
tasks Task[]
color String @default("#3b82f6")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Task {
id String @id @default(cuid())
title String
description String?
status TaskStatus @default(TODO)
priority TaskPriority @default(MEDIUM)
projectId String
project Project @relation(fields: [projectId], references: [id])
assigneeId String?
assignee User? @relation("TaskAssignee", fields: [assigneeId], references: [id])
creatorId String
creator User @relation(fields: [creatorId], references: [id])
order Int @default(0)
dueDate DateTime?
attachments Attachment[]
comments Comment[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Comment {
id String @id @default(cuid())
content String
taskId String
task Task @relation(fields: [taskId], references: [id])
authorId String
author User @relation(fields: [authorId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Attachment {
id String @id @default(cuid())
fileName String
fileUrl String
fileSize Int
mimeType String
taskId String
task Task @relation(fields: [taskId], references: [id])
uploaderId String
uploader User @relation(fields: [uploaderId], references: [id])
createdAt DateTime @default(now())
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
▶ Exemplo: Executando uma migração de banco de dados
# 1. Criar uma Migração
npx prisma migrate dev --name init
# 2. Gerar Prisma Client
npx prisma generate
# 3. Visualizar Banco de Dados
npx prisma studio
✔ Generated Prisma Client (v6.x) to .\node_modules\@prisma\client
✔ Your database is now in sync with your Prisma schema
✔ Prisma Studio opened at http://localhost:5555
5. Integração de Autenticação com Auth.js
(1) Comparação de Esquemas de Autenticação
| Solução | Vantagens | Desvantagens | Cenários Aplicáveis |
|---|---|---|---|
| Auth.js v5 | Múltiplos provedores, sessões integradas, type-safe | Curva de aprendizado moderada | Projetos que exigem autenticação flexível |
| Clerk | UI pronta para uso, sem configuração necessária | Pago, vendor lock-in | Prototipagem rápida |
| Supabase Auth | Cota gratuita generosa, integrado com o banco de dados | Requer integração com o ecossistema Supabase | Solução totalmente gerenciada |
| JWT Desenvolvido Internamente | Controle total | Altos riscos de segurança, altos custos de desenvolvimento | Não recomendado |
(2) Configuração do Auth.js
// src/lib/auth.ts
import NextAuth from "next-auth"
import Credentials from "next-auth/providers/credentials"
import Google from "next-auth/providers/google"
import { compare } from "bcryptjs"
import { prisma } from "./db"
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
Credentials({
name: "credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
return null
}
const user = await prisma.user.findUnique({
where: { email: credentials.email as string },
})
if (!user || !user.passwordHash) {
return null
}
const isValid = await compare(
credentials.password as string,
user.passwordHash
)
if (!isValid) {
return null
}
return {
id: user.id,
email: user.email,
name: user.name,
image: user.image,
}
},
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id
}
return token
},
async session({ session, token }) {
if (session.user) {
session.user.id = token.id as string
}
return session
},
},
pages: {
signIn: "/auth/signin",
error: "/auth/error",
},
session: {
strategy: "jwt",
},
})
▶ Exemplo: Rotas de API do Auth.js
// src/app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/lib/auth"
export const { GET, POST } = handlers
Saída:
Código TypeScript executado com sucesso.
// src/app/api/auth/signup/route.ts
import { NextResponse } from "next/server"
import { hash } from "bcryptjs"
import { prisma } from "@/lib/db"
export async function POST(request: Request) {
try {
const { name, email, password, inviteCode } = await request.json()
const existingUser = await prisma.user.findUnique({
where: { email },
})
if (existingUser) {
return NextResponse.json(
{ error: "Email já cadastrado" },
{ status: 400 }
)
}
const passwordHash = await hash(password, 12)
let organizationId: string | null = null
let role: "OWNER" | "ADMIN" | "MEMBER" | "VIEWER" = "MEMBER"
if (inviteCode) {
const org = await prisma.organization.findUnique({
where: { inviteCode },
})
if (org) {
organizationId = org.id
}
}
const user = await prisma.user.create({
data: {
name,
email,
passwordHash,
organizationId,
role: organizationId ? "MEMBER" : "OWNER",
},
})
if (!organizationId) {
const org = await prisma.organization.create({
data: {
name: `Organização de ${name}`,
slug: email.split("@")[0],
users: { connect: { id: user.id } },
},
})
await prisma.user.update({
where: { id: user.id },
data: { organizationId: org.id, role: "OWNER" },
})
}
return NextResponse.json(
{ message: "Usuário criado com sucesso" },
{ status: 201 }
)
} catch (error) {
return NextResponse.json(
{ error: "Algo deu errado" },
{ status: 500 }
)
}
}
(3) Sistema de Código de Convite da Organização
// src/lib/invite.ts
import { prisma } from "./db"
export async function generateInviteCode(organizationId: string) {
const org = await prisma.organization.update({
where: { id: organizationId },
data: { inviteCode: crypto.randomUUID().slice(0, 8) },
})
return org.inviteCode
}
export async function validateInviteCode(code: string) {
// Formato TKFLW-XXXXXX
const fullCode = `TKFLW-${code.toUpperCase()}`
const org = await prisma.organization.findUnique({
where: { inviteCode: fullCode },
})
return org
}
▶ Exemplo: Páginas de Login e Registro
Saída:
Atualiza o registro do banco de dados.
// src/app/(auth)/auth/signin/page.tsx
import { AuthCard } from "@/components/auth/auth-card"
import { SignInForm } from "@/components/auth/sign-in-form"
export default function SignInPage() {
return (
<div className="flex min-h-screen items-center justify-center">
<AuthCard
title="Entrar no TaskFlow"
description="Insira suas credenciais para continuar"
>
<SignInForm />
</AuthCard>
</div>
)
}
Saída:
Renderiza a UI do componente SignInPage.
// src/components/auth/sign-in-form.tsx
"use client"
import { signIn } from "next-auth/react"
import { useRouter } from "next/navigation"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Card, CardContent } from "@/components/ui/card"
export function SignInForm() {
const router = useRouter()
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
setIsLoading(true)
setError(null)
const formData = new FormData(event.currentTarget)
const result = await signIn("credentials", {
email: formData.get("email") as string,
password: formData.get("password") as string,
redirect: false,
})
if (result?.error) {
setError("Email ou senha inválidos")
setIsLoading(false)
return
}
router.push("/dashboard")
router.refresh()
}
return (
<Card>
<CardContent className="pt-6">
<form onSubmit={onSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
name="email"
type="email"
placeholder="alice@exemplo.com"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">Senha</Label>
<Input
id="password"
name="password"
type="password"
placeholder="Digite sua senha"
required
/>
</div>
{error && (
<p className="text-sm text-red-500">{error}</p>
)}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? "Entrando..." : "Entrar"}
</Button>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
Ou continue com
</span>
</div>
</div>
<Button
type="button"
variant="outline"
className="w-full"
onClick={() => signIn("google", { callbackUrl: "/dashboard" })}
>
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4"/>
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
</svg>
Entrar com Google
</Button>
</form>
</CardContent>
</Card>
)
}
6. Proteção de Rotas com Middleware
(1) Fluxo de Trabalho do Middleware
graph LR
A[Requisição] --> B{Middleware}
B -->|Caminho público| C[Permitir]
B -->|Caminho protegido| D{Tem sessão?}
D -->|Sim| C
D -->|Não| E[Redirecionar /auth/signin]
B -->|Rota de API| F{Token válido?}
F -->|Sim| G[Encaminhar]
F -->|Não| H[401 Unauthorized]
style B fill:#ffeeba
style C fill:#d4edda
style E fill:#f8d7da
(2) Implementação do Middleware
// src/middleware.ts
import { auth } from "@/lib/auth"
import { NextResponse } from "next/server"
export default auth((req) => {
const { nextUrl } = req
const isLoggedIn = !!req.auth
const isApiRoute = nextUrl.pathname.startsWith("/api")
const isAuthRoute = nextUrl.pathname.startsWith("/auth")
const isDashboardRoute = nextUrl.pathname.startsWith("/dashboard")
const isPublicRoute =
nextUrl.pathname === "/" ||
nextUrl.pathname.startsWith("/_next") ||
nextUrl.pathname.startsWith("/static") ||
nextUrl.pathname === "/api/auth/signup"
if (isApiRoute && !isPublicRoute && !isLoggedIn) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}
if (isDashboardRoute && !isLoggedIn) {
return NextResponse.redirect(new URL("/auth/signin", nextUrl))
}
if (isAuthRoute && isLoggedIn) {
return NextResponse.redirect(new URL("/dashboard", nextUrl))
}
return NextResponse.next()
})
export const config = {
matcher: [
"/((?!_next/static|_next/image|favicon.ico).*)",
],
}
▶ Exemplo: Testando a Proteção de Rotas do Middleware
# Acessando Rotas Protegidas (Não logado)
curl -v http://localhost:3000/dashboard 2>&1 | grep Location
# Visitando Rota de API (Não logado)
curl -v http://localhost:3000/api/projects 2>&1
< Location: http://localhost:3000/auth/signin
< HTTP/1.1 302 Found
{"error":"Unauthorized"}
(3) Estratégias de Configuração do Matcher
| Padrão | Caminhos Correspondentes | Caminhos Excluídos |
|---|---|---|
/((?!_next/static).*) |
Todas as Rotas | Recursos Estáticos |
/((?!api/auth).*) |
Todas as Rotas | API de Autenticação |
| `/((?!_next/static | _next/image).*)` | Todas as Rotas |
7. Seeding do Banco de Dados
(1) Estratégia de Seeding
graph TB
A[seed.ts] --> B[Criar Organization]
B --> C[Criar Users x5]
C --> D[Criar Projects x5]
D --> E[Criar Tasks x50]
E --> F[Criar Comments x40]
F --> G[Criar Attachments x10]
style A fill:#cce5ff
style G fill:#d4edda
▶ Exemplo: Script de Seeding Completo
Saída:
Diagrama do fluxo de seeding: criar organização → usuários → projetos → tarefas → comentários.
// src/prisma/seed.ts
import { PrismaClient, Role, TaskStatus, TaskPriority } from "@prisma/client"
import { hash } from "bcryptjs"
const prisma = new PrismaClient()
async function main() {
console.log("🌱 Semeando banco de dados...")
const passwordHash = await hash("password123", 12)
const org = await prisma.organization.create({
data: {
name: "Acme Corp",
slug: "acme-corp",
inviteCode: "TKFLW-ACMECORP",
},
})
const users = await Promise.all([
prisma.user.create({
data: {
name: "Alice Wang",
email: "alice@acme.com",
passwordHash,
organizationId: org.id,
role: "OWNER",
image: "https://api.dicebear.com/7.x/avataaars/svg?seed=alice",
},
}),
prisma.user.create({
data: {
name: "Bob Chen",
email: "bob@acme.com",
passwordHash,
organizationId: org.id,
role: "ADMIN",
image: "https://api.dicebear.com/7.x/avataaars/svg?seed=bob",
},
}),
prisma.user.create({
data: {
name: "Charlie Liu",
email: "charlie@acme.com",
passwordHash,
organizationId: org.id,
role: "MEMBER",
image: "https://api.dicebear.com/7.x/avataaars/svg?seed=charlie",
},
}),
prisma.user.create({
data: {
name: "Diana Park",
email: "diana@acme.com",
passwordHash,
organizationId: org.id,
role: "MEMBER",
image: "https://api.dicebear.com/7.x/avataaars/svg?seed=diana",
},
}),
prisma.user.create({
data: {
name: "Eve Zhang",
email: "eve@acme.com",
passwordHash,
organizationId: org.id,
role: "VIEWER",
image: "https://api.dicebear.com/7.x/avataaars/svg?seed=eve",
},
}),
])
const projectData = [
{ name: "Redesign do Site", description: "Redesenhar o site da empresa com UI moderna", color: "#3b82f6" },
{ name: "App Mobile v2", description: "Versão 2 do aplicativo mobile", color: "#10b981" },
{ name: "Integração de API", description: "Integrações de API de terceiros", color: "#f59e0b" },
{ name: "Migração de Dados", description: "Migrar dados do sistema legado", color: "#ef4444" },
{ name: "Auditoria de Segurança", description: "Auditoria de segurança e conformidade do Q3", color: "#8b5cf6" },
]
const projects = await Promise.all(
projectData.map((p) =>
prisma.project.create({
data: {
...p,
organizationId: org.id,
creatorId: users[0].id,
},
})
)
)
const taskTitles = [
"Configurar pipeline CI/CD", "Componentes do design system", "Escrever testes unitários",
"Implementar autenticação de usuários", "Criar schema do banco de dados", "Documentação da API",
"Otimização de desempenho", "Auditoria de acessibilidade", "Layout responsivo mobile",
"Middleware de tratamento de erros", "Validação de dados", "Funcionalidade de busca",
"Sistema de notificações", "Recurso de upload de arquivos", "Dashboard do usuário",
"Painel de administração", "Módulo de relatórios", "Templates de email",
"Integração WebSocket", "Camada de cache", "Rate limiting",
"Sistema de logging", "Estratégia de backup", "Configuração de monitoramento",
"Teste de carga",
]
const statuses: TaskStatus[] = ["TODO", "IN_PROGRESS", "DONE"]
const priorities: TaskPriority[] = ["LOW", "MEDIUM", "HIGH", "URGENT"]
for (const project of projects.slice(0, 3)) {
const taskCount = project.name === "Redesign do Site" ? 20 : 15
for (let i = 0; i < taskCount; i++) {
const task = await prisma.task.create({
data: {
title: taskTitles[i % taskTitles.length],
description: `Descrição detalhada da tarefa #${i + 1} em ${project.name}`,
status: statuses[i % statuses.length],
priority: priorities[i % priorities.length],
projectId: project.id,
assigneeId: users[i % users.length].id,
creatorId: users[0].id,
order: i,
dueDate: new Date(Date.now() + (i + 1) * 86400000 * 7),
},
})
if (i % 3 === 0) {
await prisma.comment.create({
data: {
content: `Esta tarefa está progredindo bem. É necessário revisar a implementação.`,
taskId: task.id,
authorId: users[(i + 1) % users.length].id,
},
})
}
}
}
const totalTasks = await prisma.task.count()
const totalComments = await prisma.comment.count()
console.log(`✅ Seeding concluído:
- 1 Organização (${org.name})
- ${users.length} Usuários
- ${projects.length} Projetos
- ${totalTasks} Tarefas
- ${totalComments} Comentários`)
}
main()
.catch((e) => {
console.error(e)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})
Saída:
🌱 Semeando banco de dados...
✅ Seeding concluído:
- 1 Organização (Acme Corp)
- 5 Usuários
- 5 Projetos
- 50 Tarefas
- 16 Comentários
# package.json Adicionar script de seed
# "prisma": { "seed": "tsx src/prisma/seed.ts" }
# Executar seeding
npx prisma db seed
🌱 Seeding database...
✅ Seeding complete:
- 1 Organization (Acme Corp)
- 5 Users
- 5 Projects
- 50 Tasks
- 16 Comments
8. Exemplo Completo: Todo o Processo da Inicialização do Projeto à Autenticação
# ============================================
# Processo Completo de Inicialização do TaskFlow
# ============================================
# 1. Criar um Projeto
npx create-next-app@latest taskflow --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd taskflow
# 2. Instalar Dependências
npm install next-auth@beta @prisma/client @auth/prisma-adapter bcryptjs
npm install prisma tsx --save-dev
npm install @types/bcryptjs --save-dev
# 3. Inicializar shadcn/ui
npx shadcn@latest init -d
npx shadcn@latest add button card input label select dialog dropdown-menu avatar badge separator
# 4. Inicializar Prisma
npx prisma init --datasource-provider postgresql
# Copiar conteúdo do schema.prisma
npx prisma migrate dev --name init
npx prisma generate
# 5. Configurar Variáveis de Ambiente (.env)
cat > .env << EOF
DATABASE_URL="postgresql://postgres:password@localhost:5432/taskflow"
AUTH_SECRET="my-super-secret-key-change-in-production"
AUTH_URL="http://localhost:3000"
GOOGLE_CLIENT_ID="your-google-client-id"
GOOGLE_CLIENT_SECRET="your-google-client-secret"
EOF
# 6. Executar Seeding
npx prisma db seed
# 7. Iniciar o servidor de desenvolvimento
npm run dev
Saída esperada (ao acessar http://localhost:3000 no navegador):
→ Redirecionar automaticamente para /auth/signin (Não logado)
→ Exibir o formulário de login (Email + Senha + Google)
→ Usar alice@acme.com / password123 para Fazer Login
→ Ir para /dashboard
→ Barra Lateral Exibe: Acme Corp · 5 Projetos · 50 Tarefas
❓ Perguntas Frequentes
P: Por que usar Auth.js em vez de Clerk? R: Auth.js (NextAuth v5) é uma solução open-source que suporta mais de 80 provedores e evita vendor lock-in. Clerk é adequado para prototipagem rápida, mas se torna bastante caro quando você começa a pagar. Com Auth.js, o TaskFlow pode controlar totalmente a lógica de autenticação e se integra perfeitamente com o Prisma.
P: Como sincronizar o banco de dados após modificar o schema do Prisma? R: Execute
npx prisma migrate dev --name <descrição>para criar uma nova migração; o Prisma gerará e executará automaticamente o SQL. Em um ambiente de desenvolvimento, você pode usarnpx prisma db pushpara sincronizar diretamente (sem gerar arquivos de migração).
P: Qual é o propósito do prefixo no código de convite TKFLW-? R: O prefixo identifica o código como um código de convite do TaskFlow para evitar confusão com outros sistemas. Exemplo de código de convite completo:
TKFLW-ACMECORP. A combinação do prefixo com o slug da organização torna o código de convite altamente legível e único.
P: Por que a configuração do matcher no middleware corresponde a todos os caminhos? R: A configuração do matcher
/((?!_next/static|_next/image|favicon.ico).*)corresponde a todos os caminhos de recursos não estáticos, permitindo que o middleware realize verificações de autenticação em todas as rotas de página e rotas de API. O Next.js recomenda realizar verificações de caminho dentro do middleware em vez de segmentá-las excessivamente no matcher.
P: O que é tsx no Seeding? R: tsx é o executor TypeScript (
npm i tsx -D), que pode executar arquivos .ts diretamente sem compilação. O Prisma recomenda oficialmente o tsx como executor de seed. Você também pode usar ts-node, mas o tsx é mais rápido e oferece melhor compatibilidade.
P: Por que
passwordHashé opcional no modeloUser? R: Porque o login com Google OAuth é suportado e usuários OAuth não exigem senha. Para usuários que fazem login comCredentials,passwordHashtem um valor; para usuários OAuth,passwordHashé null. Isso demonstra a flexibilidade do design do schema.
📖 Resumo
create-next-appEscolhendo um Framework: TypeScript + Tailwind + App Router + Diretório src como Ponto de Partida para Projetos de Nível Empresarial- shadcn/ui fornece uma biblioteca de componentes UI sem dependências que pode ser instalada sob demanda sem aumentar o tamanho do pacote
- Prisma Schema usa sintaxe declarativa para definir cinco modelos — User, Organization, Project, Task e Comment — além de tipos de enumeração.
- Auth.js v5 suporta autenticação dupla via Credentials e Google OAuth; a política de Sessão JWT não requer armazenamento de sessões no banco de dados.
- Middleware implementa proteção global de rotas através do helper
auth()e configuração de matcher - O sistema de código de convite da organização (prefixado com
TKFLW-) permite que membros da equipe entrem por conta própria - O seeding do banco de dados usa o driver tsx, que pode gerar mais de 100 registros de demonstração reais
📝 Exercícios
-
Exercício Básico (⭐): Siga as etapas desta lição para inicializar o projeto TaskFlow, execute
npm run devcom sucesso e visualize a página de login no navegador. Tire uma captura de tela dos resultados e salve-a. -
Exercício Avançado (⭐⭐): Adicione um modelo
Label(sistema de tags) ao Prisma Schema, estabeleça um relacionamento muitos-para-muitos comTask(através da tabela intermediáriaTaskLabel), execute a migração e gere os tipos. Implemente uma rota de API CRUD para tags. -
Desafio (⭐⭐⭐): Estenda a configuração do Auth.js para adicionar um provedor GitHub OAuth; implemente a lógica de verificação de papéis descrita no
middleware.ts— apenas usuários com os papéisOWNEReADMINpodem acessar o caminho/dashboard/admin; escreva testes unitários para verificar a lógica de proteção de papéis.