Autenticação
O APAH Assistant utiliza Better Auth para gestão de autenticação, oferecendo login seguro com email/password e suporte a múltiplos providers.
Visão Geral
O sistema de autenticação inclui:
- Login com email e password
- Gestão de sessões seguras
- Sistema de roles e permissões (RBAC)
- Alteração e reset de password
- Proteção de rotas
Configuração do Better Auth
Setup Inicial
// src/server/auth/index.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "@/server/db";
import * as schema from "@/server/db/schema";
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
schema: {
user: schema.users,
session: schema.sessions,
account: schema.accounts,
},
}),
emailAndPassword: {
enabled: true,
requireEmailVerification: false,
},
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 dias
updateAge: 60 * 60 * 24, // Atualizar a cada 24h
cookieCache: {
enabled: true,
maxAge: 60 * 5, // 5 minutos
},
},
trustedOrigins: [process.env.NEXT_PUBLIC_APP_URL!],
});
Cliente de Autenticação
// src/lib/auth/client.ts
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_APP_URL,
});
export const { signIn, signOut, signUp, useSession } = authClient;
Fluxo de Login
Página de Login
// src/app/[lang]/(auth)/login/page.tsx
"use client";
import { signIn } from "@/lib/auth/client";
import { useState } from "react";
import { useRouter } from "next/navigation";
export default function LoginPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
setError(null);
const formData = new FormData(e.currentTarget);
const email = formData.get("email") as string;
const password = formData.get("password") as string;
const result = await signIn.email({
email,
password,
});
if (result.error) {
setError(result.error.message);
setLoading(false);
return;
}
router.push("/");
};
return (
<form onSubmit={handleSubmit}>
<Input name="email" type="email" placeholder="Email" required />
<Input name="password" type="password" placeholder="Password" required />
{error && <Alert variant="destructive">{error}</Alert>}
<Button type="submit" disabled={loading}>
{loading ? "A entrar..." : "Entrar"}
</Button>
</form>
);
}
Interface de Login
┌─────────────────────────────────────────────────────────────────┐
│ │
│ APAH Assistant │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ │ │
│ │ 🔐 Login │ │
│ │ │ │
│ │ Email │ │
│ │ [________________________] │ │
│ │ │ │
│ │ Password │ │
│ │ [________________________] │ │
│ │ │ │
│ │ [ ] Manter sessão │ │
│ │ │ │
│ │ [ Entrar ] │ │
│ │ │ │
│ │ ───────────────────────────────── │ │
│ │ Esqueceu a password? │ │
│ │ │ │
│ └─────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Proteção de Rotas
Middleware
// src/middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { auth } from "@/server/auth";
export async function middleware(request: NextRequest) {
const session = await auth.api.getSession({
headers: request.headers,
});
const isAuthRoute = request.nextUrl.pathname.includes("/login");
const isProtectedRoute = !isAuthRoute && !request.nextUrl.pathname.startsWith("/api/auth");
// Se não autenticado e rota protegida, redirecionar para login
if (!session && isProtectedRoute) {
const loginUrl = new URL("/login", request.url);
loginUrl.searchParams.set("callbackUrl", request.nextUrl.pathname);
return NextResponse.redirect(loginUrl);
}
// Se autenticado e na página de login, redirecionar para home
if (session && isAuthRoute) {
return NextResponse.redirect(new URL("/", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
Proteção Server-side
// Em Server Components
import { auth } from "@/server/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
export default async function ProtectedPage() {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
redirect("/login");
}
return <div>Conteúdo protegido para {session.user.name}</div>;
}
Proteção de tRPC Procedures
// src/server/api/trpc.ts
export const protectedProcedure = t.procedure.use(async ({ ctx, next }) => {
if (!ctx.session || !ctx.session.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: {
session: { ...ctx.session, user: ctx.session.user },
},
});
});
export const adminProcedure = protectedProcedure.use(async ({ ctx, next }) => {
const isAdmin = ctx.session.user.roles?.some((r) => r.name === "admin");
if (!isAdmin) {
throw new TRPCError({ code: "FORBIDDEN" });
}
return next({ ctx });
});
Gestão de Sessões
Estrutura da Sessão
interface Session {
id: string;
userId: string;
token: string;
expiresAt: Date;
ipAddress?: string;
userAgent?: string;
createdAt: Date;
}
interface SessionUser {
id: string;
name: string;
email: string;
image?: string;
roles: Role[];
}
Hook useSession
"use client";
import { useSession } from "@/lib/auth/client";
export function UserMenu() {
const { data: session, isPending } = useSession();
if (isPending) {
return <Skeleton />;
}
if (!session) {
return <LoginButton />;
}
return (
<DropdownMenu>
<DropdownMenuTrigger>
<Avatar>
<AvatarImage src={session.user.image} />
<AvatarFallback>{session.user.name[0]}</AvatarFallback>
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>{session.user.name}</DropdownMenuItem>
<DropdownMenuItem onClick={() => signOut()}>
Sair
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
Alteração de Password
Página de Alteração
// src/app/[lang]/(auth)/change-password/page.tsx
export default function ChangePasswordPage() {
const handleSubmit = async (formData: FormData) => {
const currentPassword = formData.get("currentPassword");
const newPassword = formData.get("newPassword");
const confirmPassword = formData.get("confirmPassword");
if (newPassword !== confirmPassword) {
throw new Error("Passwords não coincidem");
}
await authClient.changePassword({
currentPassword,
newPassword,
});
};
return (
<form action={handleSubmit}>
<Input
name="currentPassword"
type="password"
placeholder="Password atual"
/>
<Input
name="newPassword"
type="password"
placeholder="Nova password"
/>
<Input
name="confirmPassword"
type="password"
placeholder="Confirmar nova password"
/>
<Button type="submit">Alterar Password</Button>
</form>
);
}
Logout
import { signOut } from "@/lib/auth/client";
import { useRouter } from "next/navigation";
function LogoutButton() {
const router = useRouter();
const handleLogout = async () => {
await signOut();
router.push("/login");
};
return (
<Button onClick={handleLogout} variant="ghost">
Sair
</Button>
);
}
Segurança
Boas Práticas Implementadas
- Hashing de Passwords - Argon2 ou bcrypt
- Sessões Seguras - Tokens seguros com expiração
- CSRF Protection - Tokens CSRF automáticos
- Rate Limiting - Proteção contra brute force
- Secure Cookies - httpOnly, secure, sameSite
Validação de Password
const passwordSchema = z
.string()
.min(8, "Mínimo 8 caracteres")
.regex(/[A-Z]/, "Pelo menos uma maiúscula")
.regex(/[a-z]/, "Pelo menos uma minúscula")
.regex(/[0-9]/, "Pelo menos um número")
.regex(/[^A-Za-z0-9]/, "Pelo menos um caractere especial");
Headers de Segurança
// next.config.js
const securityHeaders = [
{
key: "X-Frame-Options",
value: "DENY",
},
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "Referrer-Policy",
value: "origin-when-cross-origin",
},
];
Troubleshooting
Problemas Comuns
| Problema | Causa | Solução |
|---|---|---|
| "Session expired" | Sessão expirou | Fazer login novamente |
| "Invalid credentials" | Email/password incorretos | Verificar credenciais |
| Cookie não definido | Ambiente de desenvolvimento | Verificar NEXT_PUBLIC_APP_URL |
| CORS error | Origin não autorizado | Adicionar a trustedOrigins |
Debugging
// Verificar sessão no servidor
const session = await auth.api.getSession({
headers: await headers(),
});
console.log("Session:", session);
// Verificar cookies
const cookies = request.cookies.getAll();
console.log("Cookies:", cookies);