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>
85 lines
3.4 KiB
TypeScript
85 lines
3.4 KiB
TypeScript
import "server-only";
|
|
import { cookies } from "next/headers";
|
|
import { adapterPermissions } from "@/domain/spreadsheet-adapter/permissions";
|
|
import type { AuthenticatedContext, Role } from "@/domain/spreadsheet-adapter/types";
|
|
import { getDb } from "@/infra/db/client";
|
|
import { activeProviders, WORKSPACE_COOKIE, type Identity } from "./identity-provider";
|
|
import { listMemberships, resolveWorkspace, type Membership } from "./workspace";
|
|
|
|
export type AuthState =
|
|
| { kind: "anonymous" }
|
|
| { kind: "session_expired" }
|
|
| { kind: "no_workspace" }
|
|
| { kind: "not_member" }
|
|
| { kind: "workspace_choice"; memberships: Membership[] }
|
|
| { kind: "ok"; ctx: AuthenticatedContext };
|
|
|
|
export { WORKSPACE_COOKIE };
|
|
|
|
async function resolveIdentity(): Promise<{ identity: Identity | null; hadHint: boolean }> {
|
|
const jar = await cookies();
|
|
const providers = activeProviders();
|
|
let hadHint = false;
|
|
for (const provider of providers) {
|
|
if (provider.hadSessionHint(jar)) hadHint = true;
|
|
const identity = await provider.resolve(jar);
|
|
if (identity) return { identity, hadHint };
|
|
}
|
|
return { identity: null, hadHint };
|
|
}
|
|
|
|
export async function resolveAuth(): Promise<AuthState> {
|
|
const { identity, hadHint } = await resolveIdentity();
|
|
if (!identity) return { kind: hadHint ? "session_expired" : "anonymous" };
|
|
|
|
const db = await getDb();
|
|
const memberships = await listMemberships(db, identity.userId);
|
|
const resolution = resolveWorkspace(memberships, identity.workspaceHint);
|
|
if (resolution.kind === "none") return { kind: "no_workspace" };
|
|
if (resolution.kind === "choice") return { kind: "workspace_choice", memberships: resolution.memberships };
|
|
|
|
const workspaceId = resolution.workspaceId;
|
|
const rows = await db.withUser(identity.userId, (query) =>
|
|
query.query<{
|
|
display_name: string;
|
|
is_superadmin: boolean;
|
|
workspace_name: string;
|
|
role: Role;
|
|
module_enabled: boolean;
|
|
allow_upload: boolean;
|
|
}>(
|
|
`select p.display_name, p.is_superadmin, w.name as workspace_name, m.role,
|
|
coalesce(f.enabled, false) as module_enabled,
|
|
coalesce(s.allow_non_admin_model_upload, false) as allow_upload
|
|
from public.profiles p
|
|
join public.workspace_members m on m.user_id = p.id and m.workspace_id = $2
|
|
join public.workspaces w on w.id = m.workspace_id
|
|
left join public.workspace_module_flags f on f.workspace_id = w.id and f.module_key = 'spreadsheet_adapter'
|
|
left join public.spreadsheet_adapter_settings s on s.workspace_id = w.id
|
|
where p.id = $1`,
|
|
[identity.userId, workspaceId],
|
|
),
|
|
);
|
|
const row = rows[0];
|
|
if (!row) return { kind: "not_member" };
|
|
const ctx = {
|
|
userId: identity.userId,
|
|
displayName: row.display_name,
|
|
isSuperadmin: row.is_superadmin,
|
|
workspaceId,
|
|
workspaceName: row.workspace_name,
|
|
role: row.role,
|
|
moduleEnabled: row.module_enabled,
|
|
allowNonAdminModelUpload: row.allow_upload,
|
|
};
|
|
return { kind: "ok", ctx: { ...ctx, permissions: adapterPermissions(ctx) } };
|
|
}
|
|
|
|
/** Lista as associações do usuário autenticado, para a tela de escolha. */
|
|
export async function currentMemberships(): Promise<{ userId: string; memberships: Membership[] } | null> {
|
|
const { identity } = await resolveIdentity();
|
|
if (!identity) return null;
|
|
const db = await getDb();
|
|
return { userId: identity.userId, memberships: await listMemberships(db, identity.userId) };
|
|
}
|