Autentica homologação com Supabase real e resolve workspace no servidor

Duas lacunas impediam qualquer uso de homologação. Primeira: sem SSO do Elo
Pro e sem o login local, não havia como entrar. Segunda, mais grave: nada no
repositório gravava o cookie `elo_pro_workspace_id`, que era lido como origem
do workspace ativo — mesmo com sessão válida a resposta era sempre "sem
workspace".

- `identity-provider.ts` define o contrato de identidade. Trocar o Supabase de
  homologação pelo SSO do Elo Pro passa a ser acrescentar uma implementação;
  nenhum outro arquivo conhece o mecanismo.
- `/api/auth/staging/session` autentica com `signInWithPassword` no projeto
  Supabase de homologação. Há senha, emitida e validada pelo Supabase, e o
  Adaptador continua sem tabela de usuário, senha ou sessão própria.
- O login local sem senha existe apenas em `ADAPTER_ENV=development`, e não
  por configuração: fora de development o provedor nem entra na lista.
- `workspace.ts` resolve o workspace por associação real lida sob RLS. O
  cookie passa a ser preferência, não prova: aceitá-lo como autorização
  transformava um valor editável pelo cliente em credencial.
- `/api/auth/workspace` grava a preferência só depois de confirmar a
  associação, e `/selecionar-workspace` existe para quem participa de mais de
  um workspace.
- Rate limiting por usuário e bucket nas rotas do módulo, com Retry-After.
  Upload e processamento custam muito mais que leitura.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
camilafagundesandClaude Opus 5 committed 2026-10-15 20:38:45 -03:00
1 parent b972589949
commit f5560cc4ac
15 files changed
+586 -55

No files matched your search

+60
View File
@@ -0,0 +1,60 @@
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
import { isProductionEnv } from "@/config/env";
import { getDb } from "@/infra/db/client";
import { currentMemberships, WORKSPACE_COOKIE } from "@/server/auth/context";
import { assertMembership } from "@/server/auth/workspace";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
/** Associações do usuário autenticado. */
export async function GET() {
const current = await currentMemberships();
if (!current) {
return NextResponse.json({ error: { code: "UNAUTHENTICATED", message: "Sessão necessária." } }, { status: 401 });
}
return NextResponse.json({ memberships: current.memberships });
}
/**
* Grava a preferência de workspace ativo. O cookie resultante é só preferência:
* toda requisição revalida a associação no servidor. Gravar aqui sem checar
* membership transformaria o cookie em credencial.
*/
export async function POST(request: Request) {
const current = await currentMemberships();
if (!current) {
return NextResponse.json({ error: { code: "UNAUTHENTICATED", message: "Sessão necessária." } }, { status: 401 });
}
let body: { workspaceId?: string };
try {
body = (await request.json()) as { workspaceId?: string };
} catch {
return NextResponse.json({ error: { code: "INVALID_BODY", message: "Corpo inválido." } }, { status: 400 });
}
if (!body.workspaceId) {
return NextResponse.json(
{ error: { code: "WORKSPACE_REQUIRED", message: "Informe o workspace." } },
{ status: 400 },
);
}
const db = await getDb();
try {
await assertMembership(db, current.userId, body.workspaceId);
} catch {
return NextResponse.json(
{ error: { code: "WORKSPACE_FORBIDDEN", message: "Você não é membro deste workspace." } },
{ status: 403 },
);
}
const jar = await cookies();
jar.set(WORKSPACE_COOKIE, body.workspaceId, {
httpOnly: true,
sameSite: "lax",
secure: isProductionEnv() || process.env.NODE_ENV === "production",
path: "/",
maxAge: 60 * 60 * 12,
});
return NextResponse.json({ ok: true, workspaceId: body.workspaceId });
}