Files
adaptador-planilhas/src/components/workspace-picker.tsx
T
camilafagundesandClaude Opus 5 f5560cc4ac 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>
2026-10-15 20:38:45 -03:00

72 lines
2.3 KiB
TypeScript

"use client";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { Button, Card } from "./ui";
export type WorkspaceOption = {
workspaceId: string;
workspaceName: string;
role: string;
moduleEnabled: boolean;
};
/**
* Escolha do workspace ativo. A seleção grava apenas preferência: o servidor
* revalida a associação em toda requisição.
*/
export function WorkspacePicker({ memberships, next }: { memberships: WorkspaceOption[]; next: string }) {
const router = useRouter();
const [pending, setPending] = useState<string | null>(null);
const [message, setMessage] = useState<string | null>(null);
return (
<Card className="mt-6">
<h2 className="text-lg font-semibold">Selecione o workspace</h2>
<p className="mt-1 text-sm text-ink/80">
Você participa de mais de um workspace. O Adaptador trabalha em um por vez.
</p>
<div className="mt-4 space-y-2">
{memberships.map((membership) => (
<Button
key={membership.workspaceId}
variant="secondary"
className="w-full justify-between"
loading={pending === membership.workspaceId}
onClick={async () => {
setPending(membership.workspaceId);
setMessage(null);
try {
const response = await fetch("/api/auth/workspace", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ workspaceId: membership.workspaceId }),
});
if (!response.ok) {
setMessage("Não foi possível selecionar este workspace.");
return;
}
router.push(next);
router.refresh();
} finally {
setPending(null);
}
}}
>
<span>{membership.workspaceName}</span>
<span className="text-xs font-normal text-ink/70">
{membership.role}
{membership.moduleEnabled ? "" : " · módulo desligado"}
</span>
</Button>
))}
</div>
{message ? (
<p role="alert" className="mt-3 text-sm text-danger">
{message}
</p>
) : null}
</Card>
);
}