Integre NextAuth.js com tRPC: configuração completa, providers OAuth, sessões seguras e proteção de rotas profissional.
Segurança Robusta: Sessões seguras, CSRF protection e OAuth providers configurados automaticamente.
DX Excepcional: Tipos de usuário automaticamente disponíveis em todos os procedures tRPC.
// 📁 src/server/auth/config.ts
import { NextAuthOptions } from 'next-auth';
import { PrismaAdapter } from '@next-auth/prisma-adapter';
import GoogleProvider from 'next-auth/providers/google';
import GithubProvider from 'next-auth/providers/github';
import CredentialsProvider from 'next-auth/providers/credentials';
import bcrypt from 'bcryptjs';
import { prisma } from '../db/client';
export const authOptions: NextAuthOptions = {
// 🗄️ Adapter do Prisma para persistir sessões
adapter: PrismaAdapter(prisma),
// 🔐 Providers de autenticação
providers: [
// 🌐 Google OAuth
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
authorization: {
params: {
prompt: 'consent',
access_type: 'offline',
response_type: 'code',
},
},
}),
// 🐙 GitHub OAuth
GithubProvider({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}),
// 📧 Email/Password
CredentialsProvider({
name: 'credentials',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
return null;
}
// 🔍 Buscar usuário no banco
const user = await prisma.user.findUnique({
where: { email: credentials.email },
});
if (!user || !user.password) {
return null;
}
// 🔒 Verificar senha
const passwordValid = await bcrypt.compare(
credentials.password,
user.password
);
if (!passwordValid) {
return null;
}
return {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
};
},
}),
],
// 🎯 Configuração de sessão
session: {
strategy: 'jwt',
maxAge: 30 * 24 * 60 * 60, // 30 dias
},
// 🔑 JWT configuration
jwt: {
secret: process.env.NEXTAUTH_SECRET,
maxAge: 30 * 24 * 60 * 60, // 30 dias
},
// 📄 Páginas customizadas
pages: {
signIn: '/auth/signin',
signUp: '/auth/signup',
error: '/auth/error',
},
// 🎭 Callbacks para customizar sessão e JWT
callbacks: {
// 🔧 Customizar JWT token
async jwt({ token, user, account }) {
// 🆕 Primeiro login
if (user) {
token.id = user.id;
token.role = user.role;
}
// 🔗 OAuth account linking
if (account) {
token.accessToken = account.access_token;
}
return token;
},
// 🔧 Customizar sessão
async session({ session, token }) {
if (token) {
session.user.id = token.id as string;
session.user.role = token.role as string;
}
return session;
},
// 🔐 Callback de redirect
async redirect({ url, baseUrl }) {
// ✅ Permite redirects relativos e para o mesmo origin
if (url.startsWith('/')) return `${baseUrl}${url}`;
if (new URL(url).origin === baseUrl) return url;
return baseUrl;
},
},
// 🔒 Security settings
cookies: {
sessionToken: {
name: 'next-auth.session-token',
options: {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: process.env.NODE_ENV === 'production',
},
},
},
// 📝 Events e debugging
events: {
async signIn({ user, account, profile }) {
console.log(`✅ User signed in: ${user.email}`);
},
async signOut({ session }) {
console.log(`👋 User signed out: ${session?.user?.email}`);
},
},
// 🐛 Debug em desenvolvimento
debug: process.env.NODE_ENV === 'development',
};
// 📁 src/app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import { authOptions } from '@/server/auth/config';
// 🎯 Handler para todas as rotas de auth
const handler = NextAuth(authOptions);
// 📤 Exportar para GET e POST
export { handler as GET, handler as POST };
// 📁 src/types/next-auth.d.ts
import { type DefaultSession } from 'next-auth';
import { type Role } from '@prisma/client';
// 🎯 Extend dos tipos padrão do NextAuth
declare module 'next-auth' {
interface Session {
user: {
id: string;
role: Role;
} & DefaultSession['user'];
}
interface User {
id: string;
role: Role;
}
}
declare module 'next-auth/jwt' {
interface JWT {
id: string;
role: Role;
}
}
// 📁 src/components/providers/auth-provider.tsx
'use client';
import { SessionProvider } from 'next-auth/react';
import { type ReactNode } from 'react';
interface AuthProviderProps {
children: ReactNode;
}
export function AuthProvider({ children }: AuthProviderProps) {
return (
<SessionProvider
// 🔄 Refetch session every 5 minutes
refetchInterval={5 * 60}
// 🔄 Refetch when window gains focus
refetchOnWindowFocus={true}
>
{children}
</SessionProvider>
);
}
// 📁 src/app/layout.tsx (atualização)
import { AuthProvider } from '@/components/providers/auth-provider';
import { TrpcProvider } from '@/components/providers/trpc-provider';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="pt-BR">
<body>
<AuthProvider>
<TrpcProvider>
{children}
</TrpcProvider>
</AuthProvider>
</body>
</html>
);
}
// 📁 src/hooks/use-auth.ts
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
// 🔐 Hook para rotas protegidas
export function useRequireAuth(redirectTo = '/auth/signin') {
const { data: session, status } = useSession();
const router = useRouter();
useEffect(() => {
if (status === 'loading') return; // Ainda carregando
if (!session) {
router.push(redirectTo);
}
}, [session, status, router, redirectTo]);
return {
session,
isLoading: status === 'loading',
isAuthenticated: !!session,
};
}
// 👑 Hook para rotas de admin
export function useRequireAdmin(redirectTo = '/dashboard') {
const { session, isLoading, isAuthenticated } = useRequireAuth();
const router = useRouter();
useEffect(() => {
if (isLoading) return;
if (isAuthenticated && session?.user?.role !== 'ADMIN') {
router.push(redirectTo);
}
}, [session, isLoading, isAuthenticated, router, redirectTo]);
return {
session,
isLoading,
isAuthenticated,
isAdmin: session?.user?.role === 'ADMIN',
};
}
// 📁 src/lib/auth-utils.ts
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/server/auth/config';
import { type GetServerSidePropsContext } from 'next';
// 🔧 Utility para buscar sessão no servidor
export async function getServerAuthSession(ctx?: {
req: GetServerSidePropsContext['req'];
res: GetServerSidePropsContext['res'];
}) {
return await getServerSession(ctx?.req, ctx?.res, authOptions);
}
// 🛡️ HOC para proteção de páginas
export function withAuth<T extends object>(
WrappedComponent: React.ComponentType<T>,
options: {
requireAdmin?: boolean;
redirectTo?: string;
} = {}
) {
return function AuthenticatedComponent(props: T) {
const { requireAdmin = false, redirectTo = '/auth/signin' } = options;
if (requireAdmin) {
const { session, isLoading } = useRequireAdmin(redirectTo);
if (isLoading) {
return <div>Carregando...</div>;
}
if (!session) {
return null;
}
} else {
const { session, isLoading } = useRequireAuth(redirectTo);
if (isLoading) {
return <div>Carregando...</div>;
}
if (!session) {
return null;
}
}
return <WrappedComponent {...props} />;
};
}
// 📁 src/server/trpc/routers/auth.ts
import { z } from 'zod';
import bcrypt from 'bcryptjs';
import { TRPCError } from '@trpc/server';
import { router, publicProcedure, protectedProcedure } from '../trpc';
export const authRouter = router({
// 📝 Registrar novo usuário
register: publicProcedure
.input(z.object({
name: z.string().min(2).max(50),
email: z.string().email(),
password: z.string().min(8).max(100),
}))
.mutation(async ({ input, ctx }) => {
const { name, email, password } = input;
// 🔍 Verificar se usuário já existe
const existingUser = await ctx.prisma.user.findUnique({
where: { email },
});
if (existingUser) {
throw new TRPCError({
code: 'CONFLICT',
message: 'Usuário com este email já existe',
});
}
// 🔒 Hash da senha
const hashedPassword = await bcrypt.hash(password, 12);
// 💾 Criar usuário
const user = await ctx.prisma.user.create({
data: {
name,
email,
password: hashedPassword,
},
select: {
id: true,
name: true,
email: true,
role: true,
createdAt: true,
},
});
return {
user,
message: 'Usuário criado com sucesso',
};
}),
// 🔄 Atualizar senha
changePassword: protectedProcedure
.input(z.object({
currentPassword: z.string(),
newPassword: z.string().min(8).max(100),
}))
.mutation(async ({ input, ctx }) => {
const { currentPassword, newPassword } = input;
// 🔍 Buscar usuário atual
const user = await ctx.prisma.user.findUnique({
where: { id: ctx.session.user.id },
});
if (!user || !user.password) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Usuário não encontrado',
});
}
// 🔒 Verificar senha atual
const isValidPassword = await bcrypt.compare(currentPassword, user.password);
if (!isValidPassword) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Senha atual incorreta',
});
}
// 🔒 Hash da nova senha
const hashedNewPassword = await bcrypt.hash(newPassword, 12);
// 🔄 Atualizar senha
await ctx.prisma.user.update({
where: { id: ctx.session.user.id },
data: { password: hashedNewPassword },
});
return {
message: 'Senha atualizada com sucesso',
};
}),
// 👤 Buscar sessão atual
getSession: publicProcedure
.query(async ({ ctx }) => {
return ctx.session;
}),
// 🗑️ Deletar conta
deleteAccount: protectedProcedure
.input(z.object({
password: z.string(),
confirmation: z.literal('DELETE_MY_ACCOUNT'),
}))
.mutation(async ({ input, ctx }) => {
const { password, confirmation } = input;
if (confirmation !== 'DELETE_MY_ACCOUNT') {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Confirmação incorreta',
});
}
// 🔍 Buscar usuário
const user = await ctx.prisma.user.findUnique({
where: { id: ctx.session.user.id },
});
if (!user || !user.password) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Usuário não encontrado',
});
}
// 🔒 Verificar senha
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Senha incorreta',
});
}
// 🗑️ Deletar usuário (cascade delete configurado no Prisma)
await ctx.prisma.user.delete({
where: { id: ctx.session.user.id },
});
return {
message: 'Conta deletada com sucesso',
};
}),
});
// 📁 src/components/auth/signin-form.tsx
'use client';
import { useState } from 'react';
import { signIn, getSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
export function SignInForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setError('');
try {
const result = await signIn('credentials', {
email,
password,
redirect: false,
});
if (result?.error) {
setError('Email ou senha incorretos');
} else {
// ✅ Login bem-sucedido, atualizar sessão
await getSession();
router.push('/dashboard');
}
} catch (error) {
setError('Erro interno. Tente novamente.');
} finally {
setIsLoading(false);
}
};
const handleOAuthSignIn = async (provider: 'google' | 'github') => {
setIsLoading(true);
await signIn(provider, { callbackUrl: '/dashboard' });
};
return (
<div className="max-w-md mx-auto bg-gray-900 p-8 rounded-lg">
<h2 className="text-2xl font-bold text-white mb-6">Entrar</h2>
{error && (
<div className="bg-red-900/20 border border-red-400/30 text-red-400 p-3 rounded mb-4">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<Input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
disabled={isLoading}
/>
<Input
type="password"
placeholder="Senha"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
disabled={isLoading}
/>
<Button
type="submit"
className="w-full"
disabled={isLoading}
>
{isLoading ? 'Entrando...' : 'Entrar'}
</Button>
</form>
<div className="mt-6">
<div className="text-center text-gray-400 mb-4">ou</div>
<div className="space-y-2">
<Button
variant="outline"
className="w-full"
onClick={() => handleOAuthSignIn('google')}
disabled={isLoading}
>
Continuar com Google
</Button>
<Button
variant="outline"
className="w-full"
onClick={() => handleOAuthSignIn('github')}
disabled={isLoading}
>
Continuar com GitHub
</Button>
</div>
</div>
</div>
);
}
# 📄 .env.local
# NextAuth
NEXTAUTH_SECRET="seu-secret-super-seguro-aqui"
NEXTAUTH_URL="http://localhost:3000"
# Google OAuth
GOOGLE_CLIENT_ID="seu-google-client-id"
GOOGLE_CLIENT_SECRET="seu-google-client-secret"
# GitHub OAuth
GITHUB_CLIENT_ID="seu-github-client-id"
GITHUB_CLIENT_SECRET="seu-github-client-secret"
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/saas_trpc"
Type Safety:Dados do usuário tipados automaticamente em todos os procedures.
Segurança Automática:CSRF protection, secure cookies e session management.
OAuth Simples:Google, GitHub, Discord e outros providers com poucos cliques.
Middleware Integrado:Proteção de rotas automática em procedures protegidos.