From 0414a8de5d5606cc1340540f8301ae37acac7833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3ger=20Lenhart?= Date: Tue, 24 Mar 2026 10:30:02 -0300 Subject: [PATCH 1/7] :sparkles: add cognito claims via custom oauth and google providers --- .../src/features/auth/types/cognito.ts | 10 ++- .../src/pages/api/auth/[...nextauth].ts | 84 +++++++++---------- 2 files changed, 48 insertions(+), 46 deletions(-) diff --git a/apps/builder/src/features/auth/types/cognito.ts b/apps/builder/src/features/auth/types/cognito.ts index 0f57fff8df..4d4a5fae4e 100644 --- a/apps/builder/src/features/auth/types/cognito.ts +++ b/apps/builder/src/features/auth/types/cognito.ts @@ -15,11 +15,13 @@ export type CognitoJWTPayload = { email: string } +export type CognitoClaims = Pick< + CognitoJWTPayload, + 'custom:eddie_workspaces' | 'custom:hub_role' +> + type WithCognitoClaims = { - cognitoClaims?: Pick< - CognitoJWTPayload, - 'custom:eddie_workspaces' | 'custom:hub_role' - > + cognitoClaims?: CognitoClaims } // Database user extended with Cognito claims diff --git a/apps/builder/src/pages/api/auth/[...nextauth].ts b/apps/builder/src/pages/api/auth/[...nextauth].ts index 2a8b7ee52e..05fae2f9ee 100644 --- a/apps/builder/src/pages/api/auth/[...nextauth].ts +++ b/apps/builder/src/pages/api/auth/[...nextauth].ts @@ -1,4 +1,4 @@ -import NextAuth, { Account, AuthOptions } from 'next-auth' +import NextAuth, { Account, AuthOptions, User as NextAuthUser } from 'next-auth' import { JWT } from 'next-auth/jwt' import CredentialsProvider from 'next-auth/providers/credentials' import EmailProvider from 'next-auth/providers/email' @@ -27,6 +27,7 @@ import { trackEvents } from '@typebot.io/telemetry/trackEvents' import { NextAuthJWTWithCognito, DatabaseUserWithCognito, + CognitoClaims, } from '@/features/auth/types/cognito' import logger from '@/helpers/logger' import { verifyCognitoToken } from '@/features/auth/helpers/verifyCognitoToken' @@ -139,13 +140,17 @@ if (env.CUSTOM_OAUTH_WELL_KNOWN_URL) { clientId: env.CUSTOM_OAUTH_CLIENT_ID, clientSecret: env.CUSTOM_OAUTH_CLIENT_SECRET, wellKnown: env.CUSTOM_OAUTH_WELL_KNOWN_URL, - profile(profile) { + profile(profile): User & { cognitoClaims: CognitoClaims } { return { id: getAtPath(profile, env.CUSTOM_OAUTH_USER_ID_PATH), name: getAtPath(profile, env.CUSTOM_OAUTH_USER_NAME_PATH), email: getAtPath(profile, env.CUSTOM_OAUTH_USER_EMAIL_PATH), image: getAtPath(profile, env.CUSTOM_OAUTH_USER_IMAGE_PATH), - } as User + cognitoClaims: { + 'custom:hub_role': profile['custom:hub_role'], + 'custom:eddie_workspaces': profile['custom:eddie_workspaces'], + }, + } as User & { cognitoClaims: CognitoClaims } }, }) } @@ -263,7 +268,6 @@ export const getAuthOptions = ({ jwt: async ({ token, user, account }) => { const nextAuthJWT = token as NextAuthJWTWithCognito - // If user is provided (first sign in), add user info to token if (user && account) { nextAuthJWT.userId = user.id nextAuthJWT.email = user.email || undefined @@ -271,49 +275,30 @@ export const getAuthOptions = ({ nextAuthJWT.image = user.image || undefined nextAuthJWT.provider = account.provider - // Extract Cognito claims from cloudchat-embedded provider - if (account.provider === 'cloudchat-embedded') { - const userFromCognitoAuth = user as DatabaseUserWithCognito - const claimsFromCognitoToken = userFromCognitoAuth.cognitoClaims - const cloudChatAuthorization = - userFromCognitoAuth.cloudChatAuthorization - - nextAuthJWT.cloudChatAuthorization = cloudChatAuthorization - if (claimsFromCognitoToken) { - logger.debug( - 'Transferring claims from Cognito auth to NextAuth JWT', - { - hasEddieWorkspaces: - !!claimsFromCognitoToken['custom:eddie_workspaces'], - eddieWorkspacesCount: - typeof claimsFromCognitoToken['custom:eddie_workspaces'] === - 'string' - ? claimsFromCognitoToken['custom:eddie_workspaces'].split( - ',' - ).length - : 0, - } - ) - - nextAuthJWT.cognitoClaims = claimsFromCognitoToken - logger.debug('Final cognitoClaims set', { - hasHubRole: !!claimsFromCognitoToken['custom:hub_role'], - hasEddieWorkspaces: - !!claimsFromCognitoToken['custom:eddie_workspaces'], - }) - - logger.info('User authenticated via Cognito token', { - hubRole: claimsFromCognitoToken['custom:hub_role'], - hasEddieWorkspaces: - !!claimsFromCognitoToken['custom:eddie_workspaces'], - provider: 'cognito', - }) - } + const cognitoClaims = extractCognitoClaims( + account.provider, + user as NextAuthUser + ) + if (cognitoClaims) { + nextAuthJWT.cognitoClaims = cognitoClaims + logger.info('User authenticated with cognito claims', { + email: user.email, + hubRole: cognitoClaims['custom:hub_role'], + hasEddieWorkspaces: !!cognitoClaims['custom:eddie_workspaces'], + provider: account.provider, + }) } else { logger.info('User authenticated via OAuth provider', { + email: user.email, provider: account.provider, }) } + + if (account.provider === 'cloudchat-embedded') { + nextAuthJWT.cloudChatAuthorization = ( + user as DatabaseUserWithCognito + ).cloudChatAuthorization + } } return nextAuthJWT as JWT & NextAuthJWTWithCognito }, @@ -472,4 +457,19 @@ const getRequiredGroups = (provider: string): string[] => { const checkHasGroups = (userGroups: string[], requiredGroups: string[]) => userGroups?.some((userGroup) => requiredGroups?.includes(userGroup)) +const extractCognitoClaims = ( + provider: string, + user: NextAuthUser +): CognitoClaims | undefined => { + switch (provider) { + case 'google': + return { 'custom:hub_role': 'ADMIN', 'custom:eddie_workspaces': '' } + case 'cloudchat-embedded': + case 'custom-oauth': + return (user as DatabaseUserWithCognito).cognitoClaims + default: + return undefined + } +} + export default handler From e3c88cf5e472920e54c5022817db5511f90c6a54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3ger=20Lenhart?= Date: Tue, 24 Mar 2026 11:59:24 -0300 Subject: [PATCH 2/7] :zap: optimize workspace query with cognito filters --- .../features/workspace/api/listWorkspaces.ts | 47 ++++++++----------- .../workspace/helpers/cognitoUtils.ts | 18 +++++++ 2 files changed, 37 insertions(+), 28 deletions(-) diff --git a/apps/builder/src/features/workspace/api/listWorkspaces.ts b/apps/builder/src/features/workspace/api/listWorkspaces.ts index 366aeb0aad..209acfccd0 100644 --- a/apps/builder/src/features/workspace/api/listWorkspaces.ts +++ b/apps/builder/src/features/workspace/api/listWorkspaces.ts @@ -3,7 +3,7 @@ import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { workspaceSchema } from '@typebot.io/schemas' import { z } from 'zod' -import { checkCognitoWorkspaceAccess } from '../helpers/cognitoUtils' +import { getCognitoAccessibleWorkspaceIds } from '../helpers/cognitoUtils' export const listWorkspaces = authenticatedProcedure .meta({ @@ -24,35 +24,26 @@ export const listWorkspaces = authenticatedProcedure }) ) .query(async ({ ctx: { user } }) => { - // First, get workspaces where user is a member in the database - const dbWorkspaces = await prisma.workspace.findMany({ - where: { members: { some: { userId: user.id } } }, - select: { name: true, id: true, icon: true, plan: true }, + const cognitoAccess = getCognitoAccessibleWorkspaceIds(user) + + const cognitoFilter = + cognitoAccess === 'all' + ? [{}] + : cognitoAccess.length > 0 + ? [{ id: { in: cognitoAccess } }] + : [] + + const conditions = [ + { members: { some: { userId: user.id } } }, + ...cognitoFilter, + ] + + const workspaces = await prisma.workspace.findMany({ + where: { OR: conditions }, + select: { id: true, name: true, icon: true, plan: true }, }) - // Create a set of workspace IDs that user already has database access to - const dbWorkspaceIds = new Set(dbWorkspaces.map((w) => w.id)) - - // Then, check for Cognito-based workspace access - let cognitoWorkspaces: typeof dbWorkspaces = [] - - // Get workspaces that user doesn't already have database access to - const remainingWorkspaces = await prisma.workspace.findMany({ - where: { - id: { notIn: Array.from(dbWorkspaceIds) }, - }, - select: { name: true, id: true, icon: true, plan: true }, - }) - - cognitoWorkspaces = remainingWorkspaces.filter((workspace) => { - const cognitoAccess = checkCognitoWorkspaceAccess(user, workspace.id) - return cognitoAccess.hasAccess - }) - - // Combine workspaces (no need for Map since they're now guaranteed to be unique) - const workspaces = [...dbWorkspaces, ...cognitoWorkspaces] - - if (!workspaces || workspaces.length === 0) + if (workspaces.length === 0) throw new TRPCError({ code: 'NOT_FOUND', message: 'No workspaces found' }) return { workspaces } diff --git a/apps/builder/src/features/workspace/helpers/cognitoUtils.ts b/apps/builder/src/features/workspace/helpers/cognitoUtils.ts index 5ec85dd7e6..b66f02aa4a 100644 --- a/apps/builder/src/features/workspace/helpers/cognitoUtils.ts +++ b/apps/builder/src/features/workspace/helpers/cognitoUtils.ts @@ -104,6 +104,24 @@ export const mapCognitoRoleToWorkspaceRole = ( } } +export const getCognitoAccessibleWorkspaceIds = (user: { + cognitoClaims?: unknown +}): 'all' | string[] => { + const claims = extractCognitoUserClaims(user) + if (!claims) return [] + + if (claims['custom:hub_role'] === 'ADMIN') return 'all' + + if (claims['custom:eddie_workspaces']) { + return claims['custom:eddie_workspaces'] + .split(',') + .map((id) => id.trim()) + .filter(Boolean) + } + + return [] +} + /** * Centralized function to check Cognito-based workspace access and return role information. * This replaces the duplicated pattern of extracting claims, checking access, and mapping roles. From f3e50a665021cf5fe34bfce9e9f8b383ebb856f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3ger=20Lenhart?= Date: Tue, 24 Mar 2026 14:20:38 -0300 Subject: [PATCH 3/7] :white_check_mark: fix tests --- .../workspace/helpers/cognitoUtils.test.ts | 111 ++++++++++++++++-- .../src/pages/api/auth/[...nextauth].ts | 6 +- 2 files changed, 102 insertions(+), 15 deletions(-) diff --git a/apps/builder/src/features/workspace/helpers/cognitoUtils.test.ts b/apps/builder/src/features/workspace/helpers/cognitoUtils.test.ts index fe690baa2d..bb2edcd910 100644 --- a/apps/builder/src/features/workspace/helpers/cognitoUtils.test.ts +++ b/apps/builder/src/features/workspace/helpers/cognitoUtils.test.ts @@ -15,6 +15,7 @@ import { mapCognitoRoleToWorkspaceRole, hasWorkspaceAccess, checkCognitoWorkspaceAccess, + getCognitoAccessibleWorkspaceIds, } from './cognitoUtils' describe('extractCognitoUserClaims', () => { @@ -131,26 +132,26 @@ describe('mapCognitoRoleToWorkspaceRole', () => { expect(mapCognitoRoleToWorkspaceRole('CLIENT')).toBe(WorkspaceRole.MEMBER) }) - it('should default unknown roles to MEMBER', () => { - expect(mapCognitoRoleToWorkspaceRole('UNKNOWN')).toBe(WorkspaceRole.MEMBER) + it('should default unknown roles to GUEST', () => { + expect(mapCognitoRoleToWorkspaceRole('UNKNOWN')).toBe(WorkspaceRole.GUEST) }) it('should handle empty string', () => { - expect(mapCognitoRoleToWorkspaceRole('')).toBe(WorkspaceRole.MEMBER) + expect(mapCognitoRoleToWorkspaceRole('')).toBe(WorkspaceRole.GUEST) }) it('should handle null/undefined gracefully', () => { expect(mapCognitoRoleToWorkspaceRole(null as unknown as string)).toBe( - WorkspaceRole.MEMBER + WorkspaceRole.GUEST ) expect(mapCognitoRoleToWorkspaceRole(undefined as unknown as string)).toBe( - WorkspaceRole.MEMBER + WorkspaceRole.GUEST ) }) it('should be case sensitive', () => { - expect(mapCognitoRoleToWorkspaceRole('admin')).toBe(WorkspaceRole.MEMBER) - expect(mapCognitoRoleToWorkspaceRole('Admin')).toBe(WorkspaceRole.MEMBER) + expect(mapCognitoRoleToWorkspaceRole('admin')).toBe(WorkspaceRole.GUEST) + expect(mapCognitoRoleToWorkspaceRole('Admin')).toBe(WorkspaceRole.GUEST) }) }) @@ -225,9 +226,9 @@ describe('hasWorkspaceAccess', () => { expect(hasWorkspaceAccess(claims, 'ws-123')).toBe(true) }) - it('should use exact match (case-sensitive) for workspace id', () => { + it('should use exact match (case-sensitive) for workspace id in eddie_workspaces', () => { const claims = { - 'custom:hub_role': 'ADMIN' as const, + 'custom:hub_role': 'CLIENT' as const, 'custom:eddie_workspaces': 'ws-123,WS-456', } @@ -263,7 +264,7 @@ describe('checkCognitoWorkspaceAccess', () => { }) }) - it('should return hasAccess false when workspace id is not in eddie_workspaces', () => { + it('should return hasAccess true for ADMIN even when workspace id is not in eddie_workspaces', () => { const user = { id: 'user123', email: 'test@example.com', @@ -273,9 +274,9 @@ describe('checkCognitoWorkspaceAccess', () => { }, } - expect(checkCognitoWorkspaceAccess(user, 'ws-123')).toEqual({ - hasAccess: false, - }) + const result = checkCognitoWorkspaceAccess(user, 'ws-123') + expect(result.hasAccess).toBe(true) + expect(result.role).toBe(WorkspaceRole.ADMIN) }) it('should return hasAccess true with mapped role when hub_role is present', () => { @@ -326,3 +327,87 @@ describe('checkCognitoWorkspaceAccess', () => { expect(result.role).toBe(WorkspaceRole.MEMBER) }) }) + +describe('getCognitoAccessibleWorkspaceIds', () => { + it('should return "all" when user is ADMIN', () => { + const user = { + cognitoClaims: { + 'custom:hub_role': 'ADMIN', + 'custom:eddie_workspaces': 'ws-123,ws-456', + }, + } + + expect(getCognitoAccessibleWorkspaceIds(user)).toBe('all') + }) + + it('should return parsed workspace IDs for non-admin with eddie_workspaces', () => { + const user = { + cognitoClaims: { + 'custom:hub_role': 'CLIENT', + 'custom:eddie_workspaces': 'ws-123,ws-456,ws-789', + }, + } + + expect(getCognitoAccessibleWorkspaceIds(user)).toEqual([ + 'ws-123', + 'ws-456', + 'ws-789', + ]) + }) + + it('should trim whitespace from workspace IDs', () => { + const user = { + cognitoClaims: { + 'custom:eddie_workspaces': ' ws-123 , ws-456 , ws-789 ', + }, + } + + expect(getCognitoAccessibleWorkspaceIds(user)).toEqual([ + 'ws-123', + 'ws-456', + 'ws-789', + ]) + }) + + it('should filter out empty strings from workspace IDs', () => { + const user = { + cognitoClaims: { + 'custom:eddie_workspaces': 'ws-123,,ws-456,', + }, + } + + expect(getCognitoAccessibleWorkspaceIds(user)).toEqual(['ws-123', 'ws-456']) + }) + + it('should return empty array when no cognito claims', () => { + const user = {} + + expect(getCognitoAccessibleWorkspaceIds(user)).toEqual([]) + }) + + it('should return empty array when cognitoClaims is undefined', () => { + const user = { cognitoClaims: undefined } + + expect(getCognitoAccessibleWorkspaceIds(user)).toEqual([]) + }) + + it('should return empty array for non-admin without eddie_workspaces', () => { + const user = { + cognitoClaims: { + 'custom:hub_role': 'CLIENT', + }, + } + + expect(getCognitoAccessibleWorkspaceIds(user)).toEqual([]) + }) + + it('should return single workspace ID correctly', () => { + const user = { + cognitoClaims: { + 'custom:eddie_workspaces': 'ws-123', + }, + } + + expect(getCognitoAccessibleWorkspaceIds(user)).toEqual(['ws-123']) + }) +}) diff --git a/apps/builder/src/pages/api/auth/[...nextauth].ts b/apps/builder/src/pages/api/auth/[...nextauth].ts index 05fae2f9ee..d54232edc3 100644 --- a/apps/builder/src/pages/api/auth/[...nextauth].ts +++ b/apps/builder/src/pages/api/auth/[...nextauth].ts @@ -13,7 +13,7 @@ import { Provider } from 'next-auth/providers' import { NextApiRequest, NextApiResponse } from 'next' import { customAdapter } from '../../../features/auth/api/customAdapter' import { User } from '@typebot.io/prisma' -import { getAtPath, isDefined } from '@typebot.io/lib' +import { getAtPath, isDefined, emailIsCloudhumans } from '@typebot.io/lib' import { mockedUser } from '@typebot.io/lib/mockedUser' import { getNewUserInvitations } from '@/features/auth/helpers/getNewUserInvitations' import { sendVerificationRequest } from '@/features/auth/helpers/sendVerificationRequest' @@ -463,7 +463,9 @@ const extractCognitoClaims = ( ): CognitoClaims | undefined => { switch (provider) { case 'google': - return { 'custom:hub_role': 'ADMIN', 'custom:eddie_workspaces': '' } + if (emailIsCloudhumans(user.email)) + return { 'custom:hub_role': 'ADMIN', 'custom:eddie_workspaces': '' } + return undefined case 'cloudchat-embedded': case 'custom-oauth': return (user as DatabaseUserWithCognito).cognitoClaims From 6e4b089f32af3d7e6d8d1c4a16f1fd48424fa836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3ger=20Lenhart?= Date: Tue, 24 Mar 2026 18:34:19 -0300 Subject: [PATCH 4/7] :bug: fix cognito profile handling --- .../src/features/auth/api/customAdapter.ts | 6 +++- .../src/features/auth/types/cognito.ts | 1 + .../features/workspace/api/listWorkspaces.ts | 30 ++++++++++++------- .../src/pages/api/auth/[...nextauth].ts | 17 ++++++++--- 4 files changed, 38 insertions(+), 16 deletions(-) diff --git a/apps/builder/src/features/auth/api/customAdapter.ts b/apps/builder/src/features/auth/api/customAdapter.ts index 6a2e33c02f..e82cb79ff2 100644 --- a/apps/builder/src/features/auth/api/customAdapter.ts +++ b/apps/builder/src/features/auth/api/customAdapter.ts @@ -38,9 +38,13 @@ export function customAdapter(p: PrismaClient): Adapter { name: data.name ? `${data.name}'s workspace` : `My workspace`, plan: parseWorkspaceDefaultPlan(data.email), } + const createdUser = await p.user.create({ data: { - ...data, + email: data.email, + name: data.name, + image: data.image, + emailVerified: data.emailVerified, id: user.id, apiTokens: { create: { name: 'Default', token: generateId(24) }, diff --git a/apps/builder/src/features/auth/types/cognito.ts b/apps/builder/src/features/auth/types/cognito.ts index 4d4a5fae4e..00bb4fd1e9 100644 --- a/apps/builder/src/features/auth/types/cognito.ts +++ b/apps/builder/src/features/auth/types/cognito.ts @@ -36,4 +36,5 @@ export type NextAuthJWTWithCognito = Record & name?: string image?: string provider?: string + cloudChatAuthorization?: boolean } diff --git a/apps/builder/src/features/workspace/api/listWorkspaces.ts b/apps/builder/src/features/workspace/api/listWorkspaces.ts index 209acfccd0..760eaeb6da 100644 --- a/apps/builder/src/features/workspace/api/listWorkspaces.ts +++ b/apps/builder/src/features/workspace/api/listWorkspaces.ts @@ -3,6 +3,7 @@ import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { workspaceSchema } from '@typebot.io/schemas' import { z } from 'zod' +import { Prisma } from '@typebot.io/prisma' import { getCognitoAccessibleWorkspaceIds } from '../helpers/cognitoUtils' export const listWorkspaces = authenticatedProcedure @@ -26,20 +27,27 @@ export const listWorkspaces = authenticatedProcedure .query(async ({ ctx: { user } }) => { const cognitoAccess = getCognitoAccessibleWorkspaceIds(user) - const cognitoFilter = - cognitoAccess === 'all' - ? [{}] - : cognitoAccess.length > 0 - ? [{ id: { in: cognitoAccess } }] - : [] + const personalWorkspaceFilter: Prisma.WorkspaceWhereInput = { + NOT: { + name: { contains: "'s workspace" }, + }, + } - const conditions = [ - { members: { some: { userId: user.id } } }, - ...cognitoFilter, - ] + let whereClause: Prisma.WorkspaceWhereInput + const memberFilter = { members: { some: { userId: user.id } } } + + if (cognitoAccess === 'all') { + whereClause = personalWorkspaceFilter + } else if (cognitoAccess.length > 0) { + whereClause = { + OR: [memberFilter, { id: { in: cognitoAccess } }], + } + } else { + whereClause = memberFilter + } const workspaces = await prisma.workspace.findMany({ - where: { OR: conditions }, + where: whereClause, select: { id: true, name: true, icon: true, plan: true }, }) diff --git a/apps/builder/src/pages/api/auth/[...nextauth].ts b/apps/builder/src/pages/api/auth/[...nextauth].ts index d54232edc3..3c83e6a1cc 100644 --- a/apps/builder/src/pages/api/auth/[...nextauth].ts +++ b/apps/builder/src/pages/api/auth/[...nextauth].ts @@ -265,7 +265,7 @@ export const getAuthOptions = ({ }, }, callbacks: { - jwt: async ({ token, user, account }) => { + jwt: async ({ token, user, account, profile }) => { const nextAuthJWT = token as NextAuthJWTWithCognito if (user && account) { @@ -277,7 +277,8 @@ export const getAuthOptions = ({ const cognitoClaims = extractCognitoClaims( account.provider, - user as NextAuthUser + user as NextAuthUser, + profile as Partial ) if (cognitoClaims) { nextAuthJWT.cognitoClaims = cognitoClaims @@ -459,15 +460,23 @@ const checkHasGroups = (userGroups: string[], requiredGroups: string[]) => const extractCognitoClaims = ( provider: string, - user: NextAuthUser + user: NextAuthUser, + profile?: Partial ): CognitoClaims | undefined => { switch (provider) { case 'google': if (emailIsCloudhumans(user.email)) return { 'custom:hub_role': 'ADMIN', 'custom:eddie_workspaces': '' } return undefined - case 'cloudchat-embedded': case 'custom-oauth': + if (profile) { + return { + 'custom:hub_role': profile['custom:hub_role'] ?? 'CLIENT', + 'custom:eddie_workspaces': profile['custom:eddie_workspaces'] ?? '', + } + } + return (user as DatabaseUserWithCognito).cognitoClaims + case 'cloudchat-embedded': return (user as DatabaseUserWithCognito).cognitoClaims default: return undefined From 5e28643a600c5b87eeffb17d65712e6971fe55a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3ger=20Lenhart?= Date: Wed, 25 Mar 2026 16:15:14 -0300 Subject: [PATCH 5/7] :recycle: refactor workspace query --- .../features/workspace/api/listWorkspaces.ts | 65 ++++++++++++------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/apps/builder/src/features/workspace/api/listWorkspaces.ts b/apps/builder/src/features/workspace/api/listWorkspaces.ts index 760eaeb6da..26d4bfe3a2 100644 --- a/apps/builder/src/features/workspace/api/listWorkspaces.ts +++ b/apps/builder/src/features/workspace/api/listWorkspaces.ts @@ -3,7 +3,6 @@ import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { workspaceSchema } from '@typebot.io/schemas' import { z } from 'zod' -import { Prisma } from '@typebot.io/prisma' import { getCognitoAccessibleWorkspaceIds } from '../helpers/cognitoUtils' export const listWorkspaces = authenticatedProcedure @@ -27,32 +26,50 @@ export const listWorkspaces = authenticatedProcedure .query(async ({ ctx: { user } }) => { const cognitoAccess = getCognitoAccessibleWorkspaceIds(user) - const personalWorkspaceFilter: Prisma.WorkspaceWhereInput = { - NOT: { - name: { contains: "'s workspace" }, - }, - } - - let whereClause: Prisma.WorkspaceWhereInput - const memberFilter = { members: { some: { userId: user.id } } } - - if (cognitoAccess === 'all') { - whereClause = personalWorkspaceFilter - } else if (cognitoAccess.length > 0) { - whereClause = { - OR: [memberFilter, { id: { in: cognitoAccess } }], - } - } else { - whereClause = memberFilter - } - - const workspaces = await prisma.workspace.findMany({ - where: whereClause, - select: { id: true, name: true, icon: true, plan: true }, - }) + const workspaces = await findWorkspaces(user.id, cognitoAccess) if (workspaces.length === 0) throw new TRPCError({ code: 'NOT_FOUND', message: 'No workspaces found' }) return { workspaces } }) + +const workspaceSelect = { + id: true, + name: true, + icon: true, + plan: true, +} as const + +const findAllNonPersonalWorkspaces = () => + prisma.workspace.findMany({ + where: { NOT: { name: { contains: "'s workspace" } } }, + select: workspaceSelect, + }) + +const findMemberWorkspaces = (userId: string) => + prisma.workspace.findMany({ + where: { members: { some: { userId } } }, + select: workspaceSelect, + }) + +const findMemberOrCognitoWorkspaces = ( + userId: string, + cognitoWorkspaceIds: string[] +) => + prisma.workspace.findMany({ + where: { + OR: [ + { members: { some: { userId } } }, + { id: { in: cognitoWorkspaceIds } }, + ], + }, + select: workspaceSelect, + }) + +const findWorkspaces = (userId: string, cognitoAccess: 'all' | string[]) => { + if (cognitoAccess === 'all') return findAllNonPersonalWorkspaces() + if (cognitoAccess.length > 0) + return findMemberOrCognitoWorkspaces(userId, cognitoAccess) + return findMemberWorkspaces(userId) +} From da1bd9927656134495355fbede52dd395edb3c02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3ger=20Lenhart?= Date: Wed, 25 Mar 2026 16:52:19 -0300 Subject: [PATCH 6/7] :recycle: remove profile changes --- apps/builder/src/features/auth/api/customAdapter.ts | 5 +---- apps/builder/src/pages/api/auth/[...nextauth].ts | 8 ++------ 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/apps/builder/src/features/auth/api/customAdapter.ts b/apps/builder/src/features/auth/api/customAdapter.ts index e82cb79ff2..fcb95b12bd 100644 --- a/apps/builder/src/features/auth/api/customAdapter.ts +++ b/apps/builder/src/features/auth/api/customAdapter.ts @@ -41,10 +41,7 @@ export function customAdapter(p: PrismaClient): Adapter { const createdUser = await p.user.create({ data: { - email: data.email, - name: data.name, - image: data.image, - emailVerified: data.emailVerified, + ...data, id: user.id, apiTokens: { create: { name: 'Default', token: generateId(24) }, diff --git a/apps/builder/src/pages/api/auth/[...nextauth].ts b/apps/builder/src/pages/api/auth/[...nextauth].ts index 3c83e6a1cc..e0f411ab43 100644 --- a/apps/builder/src/pages/api/auth/[...nextauth].ts +++ b/apps/builder/src/pages/api/auth/[...nextauth].ts @@ -140,17 +140,13 @@ if (env.CUSTOM_OAUTH_WELL_KNOWN_URL) { clientId: env.CUSTOM_OAUTH_CLIENT_ID, clientSecret: env.CUSTOM_OAUTH_CLIENT_SECRET, wellKnown: env.CUSTOM_OAUTH_WELL_KNOWN_URL, - profile(profile): User & { cognitoClaims: CognitoClaims } { + profile(profile) { return { id: getAtPath(profile, env.CUSTOM_OAUTH_USER_ID_PATH), name: getAtPath(profile, env.CUSTOM_OAUTH_USER_NAME_PATH), email: getAtPath(profile, env.CUSTOM_OAUTH_USER_EMAIL_PATH), image: getAtPath(profile, env.CUSTOM_OAUTH_USER_IMAGE_PATH), - cognitoClaims: { - 'custom:hub_role': profile['custom:hub_role'], - 'custom:eddie_workspaces': profile['custom:eddie_workspaces'], - }, - } as User & { cognitoClaims: CognitoClaims } + } as User }, }) } From 3797b067902dd6aa19936ec968b254be82746c07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3ger=20Lenhart?= Date: Wed, 25 Mar 2026 18:29:13 -0300 Subject: [PATCH 7/7] :recycle: refactor getCognitoAccessibleWorkspaceIds --- .../features/workspace/api/listWorkspaces.ts | 75 ++++++++++--------- .../workspace/helpers/cognitoUtils.test.ts | 46 ++++++------ .../workspace/helpers/cognitoUtils.ts | 18 +++-- .../src/pages/api/auth/[...nextauth].ts | 2 +- 4 files changed, 76 insertions(+), 65 deletions(-) diff --git a/apps/builder/src/features/workspace/api/listWorkspaces.ts b/apps/builder/src/features/workspace/api/listWorkspaces.ts index 26d4bfe3a2..5e0f65b52b 100644 --- a/apps/builder/src/features/workspace/api/listWorkspaces.ts +++ b/apps/builder/src/features/workspace/api/listWorkspaces.ts @@ -3,7 +3,11 @@ import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { workspaceSchema } from '@typebot.io/schemas' import { z } from 'zod' -import { getCognitoAccessibleWorkspaceIds } from '../helpers/cognitoUtils' +import { Prisma } from '@typebot.io/prisma' +import { + getCognitoAccessibleWorkspaceIds, + type CognitoAccess, +} from '../helpers/cognitoUtils' export const listWorkspaces = authenticatedProcedure .meta({ @@ -25,7 +29,6 @@ export const listWorkspaces = authenticatedProcedure ) .query(async ({ ctx: { user } }) => { const cognitoAccess = getCognitoAccessibleWorkspaceIds(user) - const workspaces = await findWorkspaces(user.id, cognitoAccess) if (workspaces.length === 0) @@ -34,42 +37,40 @@ export const listWorkspaces = authenticatedProcedure return { workspaces } }) -const workspaceSelect = { - id: true, - name: true, - icon: true, - plan: true, -} as const - -const findAllNonPersonalWorkspaces = () => - prisma.workspace.findMany({ - where: { NOT: { name: { contains: "'s workspace" } } }, - select: workspaceSelect, - }) - -const findMemberWorkspaces = (userId: string) => - prisma.workspace.findMany({ - where: { members: { some: { userId } } }, - select: workspaceSelect, - }) - -const findMemberOrCognitoWorkspaces = ( - userId: string, - cognitoWorkspaceIds: string[] -) => - prisma.workspace.findMany({ - where: { - OR: [ - { members: { some: { userId } } }, - { id: { in: cognitoWorkspaceIds } }, - ], +const findWorkspaces = (userId: string, cognitoAccess: CognitoAccess) => { + const workspaceFilter = getWorkspaceFilter(userId, cognitoAccess) + return prisma.workspace.findMany({ + where: workspaceFilter, + select: { + id: true, + name: true, + icon: true, + plan: true, }, - select: workspaceSelect, }) +} -const findWorkspaces = (userId: string, cognitoAccess: 'all' | string[]) => { - if (cognitoAccess === 'all') return findAllNonPersonalWorkspaces() - if (cognitoAccess.length > 0) - return findMemberOrCognitoWorkspaces(userId, cognitoAccess) - return findMemberWorkspaces(userId) +const getWorkspaceFilter = ( + userId: string, + cognitoAccess: CognitoAccess +): Prisma.WorkspaceWhereInput => { + switch (cognitoAccess.type) { + case 'admin': + return { + NOT: { name: { contains: "'s workspace" } }, + } + case 'restricted': + return { + OR: [ + { + members: { some: { userId } }, + }, + { id: { in: cognitoAccess.ids } }, + ], + } + case 'none': + return { + members: { some: { userId } }, + } + } } diff --git a/apps/builder/src/features/workspace/helpers/cognitoUtils.test.ts b/apps/builder/src/features/workspace/helpers/cognitoUtils.test.ts index bb2edcd910..d8876317aa 100644 --- a/apps/builder/src/features/workspace/helpers/cognitoUtils.test.ts +++ b/apps/builder/src/features/workspace/helpers/cognitoUtils.test.ts @@ -329,7 +329,7 @@ describe('checkCognitoWorkspaceAccess', () => { }) describe('getCognitoAccessibleWorkspaceIds', () => { - it('should return "all" when user is ADMIN', () => { + it('should return { type: "all" } when user is ADMIN', () => { const user = { cognitoClaims: { 'custom:hub_role': 'ADMIN', @@ -337,10 +337,10 @@ describe('getCognitoAccessibleWorkspaceIds', () => { }, } - expect(getCognitoAccessibleWorkspaceIds(user)).toBe('all') + expect(getCognitoAccessibleWorkspaceIds(user)).toEqual({ type: 'admin' }) }) - it('should return parsed workspace IDs for non-admin with eddie_workspaces', () => { + it('should return specific workspace IDs for non-admin with eddie_workspaces', () => { const user = { cognitoClaims: { 'custom:hub_role': 'CLIENT', @@ -348,11 +348,10 @@ describe('getCognitoAccessibleWorkspaceIds', () => { }, } - expect(getCognitoAccessibleWorkspaceIds(user)).toEqual([ - 'ws-123', - 'ws-456', - 'ws-789', - ]) + expect(getCognitoAccessibleWorkspaceIds(user)).toEqual({ + type: 'restricted', + ids: ['ws-123', 'ws-456', 'ws-789'], + }) }) it('should trim whitespace from workspace IDs', () => { @@ -362,11 +361,10 @@ describe('getCognitoAccessibleWorkspaceIds', () => { }, } - expect(getCognitoAccessibleWorkspaceIds(user)).toEqual([ - 'ws-123', - 'ws-456', - 'ws-789', - ]) + expect(getCognitoAccessibleWorkspaceIds(user)).toEqual({ + type: 'restricted', + ids: ['ws-123', 'ws-456', 'ws-789'], + }) }) it('should filter out empty strings from workspace IDs', () => { @@ -376,29 +374,32 @@ describe('getCognitoAccessibleWorkspaceIds', () => { }, } - expect(getCognitoAccessibleWorkspaceIds(user)).toEqual(['ws-123', 'ws-456']) + expect(getCognitoAccessibleWorkspaceIds(user)).toEqual({ + type: 'restricted', + ids: ['ws-123', 'ws-456'], + }) }) - it('should return empty array when no cognito claims', () => { + it('should return { type: "none" } when no cognito claims', () => { const user = {} - expect(getCognitoAccessibleWorkspaceIds(user)).toEqual([]) + expect(getCognitoAccessibleWorkspaceIds(user)).toEqual({ type: 'none' }) }) - it('should return empty array when cognitoClaims is undefined', () => { + it('should return { type: "none" } when cognitoClaims is undefined', () => { const user = { cognitoClaims: undefined } - expect(getCognitoAccessibleWorkspaceIds(user)).toEqual([]) + expect(getCognitoAccessibleWorkspaceIds(user)).toEqual({ type: 'none' }) }) - it('should return empty array for non-admin without eddie_workspaces', () => { + it('should return { type: "none" } for non-admin without eddie_workspaces', () => { const user = { cognitoClaims: { 'custom:hub_role': 'CLIENT', }, } - expect(getCognitoAccessibleWorkspaceIds(user)).toEqual([]) + expect(getCognitoAccessibleWorkspaceIds(user)).toEqual({ type: 'none' }) }) it('should return single workspace ID correctly', () => { @@ -408,6 +409,9 @@ describe('getCognitoAccessibleWorkspaceIds', () => { }, } - expect(getCognitoAccessibleWorkspaceIds(user)).toEqual(['ws-123']) + expect(getCognitoAccessibleWorkspaceIds(user)).toEqual({ + type: 'restricted', + ids: ['ws-123'], + }) }) }) diff --git a/apps/builder/src/features/workspace/helpers/cognitoUtils.ts b/apps/builder/src/features/workspace/helpers/cognitoUtils.ts index b66f02aa4a..bfb178be6f 100644 --- a/apps/builder/src/features/workspace/helpers/cognitoUtils.ts +++ b/apps/builder/src/features/workspace/helpers/cognitoUtils.ts @@ -12,6 +12,11 @@ export interface CognitoUserClaims { 'custom:eddie_workspaces'?: string } +export type CognitoAccess = + | { type: 'admin' } + | { type: 'restricted'; ids: string[] } + | { type: 'none' } + export const extractCognitoUserClaims = ( user: unknown ): CognitoUserClaims | undefined => { @@ -105,21 +110,22 @@ export const mapCognitoRoleToWorkspaceRole = ( } export const getCognitoAccessibleWorkspaceIds = (user: { - cognitoClaims?: unknown -}): 'all' | string[] => { + cognitoClaims?: CognitoUserClaims +}): CognitoAccess => { const claims = extractCognitoUserClaims(user) - if (!claims) return [] + if (!claims) return { type: 'none' } - if (claims['custom:hub_role'] === 'ADMIN') return 'all' + if (claims['custom:hub_role'] === 'ADMIN') return { type: 'admin' } if (claims['custom:eddie_workspaces']) { - return claims['custom:eddie_workspaces'] + const ids = claims['custom:eddie_workspaces'] .split(',') .map((id) => id.trim()) .filter(Boolean) + if (ids.length > 0) return { type: 'restricted', ids } } - return [] + return { type: 'none' } } /** diff --git a/apps/builder/src/pages/api/auth/[...nextauth].ts b/apps/builder/src/pages/api/auth/[...nextauth].ts index e0f411ab43..0651229d41 100644 --- a/apps/builder/src/pages/api/auth/[...nextauth].ts +++ b/apps/builder/src/pages/api/auth/[...nextauth].ts @@ -285,7 +285,7 @@ export const getAuthOptions = ({ provider: account.provider, }) } else { - logger.info('User authenticated via OAuth provider', { + logger.info('User authenticated without cognito claims', { email: user.email, provider: account.provider, })