1/** 2 * TASK-0131 — SSO ENFORCE mode (org policy `ssoEnforced`). 3 * 4 * The check is a module-AGNOSTIC free function (takes a Prisma client, no 5 * Nest DI) so it can be called from `AuthService.login`/`register` WITHOUT 6 * creating an Auth→Sso→Auth module cycle (the SSO provisioning service 7 * already depends on `AuthService` for session minting). `AuthService` holds 8 * a `PrismaService`, so it passes its own client in. 9 * 10 * Resolution (per the task spec): email → user → org memberships → the orgs 11 * that have an ACTIVE, ENFORCED `SsoProvider`. A user with NO org (or an 12 * unknown email) resolves to [] → normal password login (the fast path). 13 * The mirror invariant (spec: "mirrored into SsoProvider.enforce as the 14 * source of truth"): the admin policy editor writes BOTH `Organization.policy 15 * .ssoEnforced` and `SsoProvider.enforce`; this reads `SsoProvider.enforce` 16 * (the source of truth), so the two cannot drift at login time. 17 */ 18import type { PrismaClient } from "@prisma/client"; 19 20export interface EnforcedOrg { 21 orgId: string; 22 slug: string; 23 type: "SAML" | "OIDC"; 24} 25 26export async function resolveEnforcedSsoOrgs( 27 prisma: PrismaClient, 28 email: string 29): Promise<EnforcedOrg[]> { 30 const user = await prisma.user.findUnique({ 31 where: { email: email.toLowerCase() }, 32 select: { orgMemberships: { select: { organizationId: true } } } 33 }); 34 if (!user || user.orgMemberships.length === 0) return []; 35 const providers = await prisma.ssoProvider.findMany({ 36 where: { 37 organizationId: { in: user.orgMemberships.map((m) => m.organizationId) }, 38 enforce: true 39 }, 40 select: { 41 organizationId: true, 42 type: true, 43 organization: { select: { slug: true } } 44 } 45 }); 46 return providers.map((p) => ({ 47 orgId: p.organizationId, 48 slug: p.organization.slug, 49 type: p.type 50 })); 51} 52