Skip to main content

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

  1. Hashing de Passwords - Argon2 ou bcrypt
  2. Sessões Seguras - Tokens seguros com expiração
  3. CSRF Protection - Tokens CSRF automáticos
  4. Rate Limiting - Proteção contra brute force
  5. 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

ProblemaCausaSolução
"Session expired"Sessão expirouFazer login novamente
"Invalid credentials"Email/password incorretosVerificar credenciais
Cookie não definidoAmbiente de desenvolvimentoVerificar NEXT_PUBLIC_APP_URL
CORS errorOrigin não autorizadoAdicionar 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);