diff --git a/apps/client/src/features/auth/components/invite-sign-up-form.tsx b/apps/client/src/features/auth/components/invite-sign-up-form.tsx index 7e4d241d..2d7b3657 100644 --- a/apps/client/src/features/auth/components/invite-sign-up-form.tsx +++ b/apps/client/src/features/auth/components/invite-sign-up-form.tsx @@ -18,6 +18,7 @@ import classes from "@/features/auth/components/auth.module.css"; import { useGetInvitationQuery } from "@/features/workspace/queries/workspace-query.ts"; import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts"; import { useTranslation } from "react-i18next"; +import SsoLogin from "@/ee/components/sso-login.tsx"; const formSchema = z.object({ name: z.string().trim().min(1), @@ -71,39 +72,43 @@ export function InviteSignUpForm() { {t("Join the workspace")} - -
- + - + {!invitation.enforceSso && ( + + + - - - - + + + + + +
+ )} ); diff --git a/apps/client/src/features/workspace/queries/workspace-query.ts b/apps/client/src/features/workspace/queries/workspace-query.ts index 0add1d0a..6bf35aec 100644 --- a/apps/client/src/features/workspace/queries/workspace-query.ts +++ b/apps/client/src/features/workspace/queries/workspace-query.ts @@ -173,7 +173,7 @@ export function useRevokeInvitationMutation() { export function useGetInvitationQuery( invitationId: string, -): UseQueryResult { +): UseQueryResult { return useQuery({ queryKey: ["invitations", invitationId], queryFn: () => getInvitationById({ invitationId }), diff --git a/apps/client/src/features/workspace/types/workspace.types.ts b/apps/client/src/features/workspace/types/workspace.types.ts index 4112ee5c..730106ce 100644 --- a/apps/client/src/features/workspace/types/workspace.types.ts +++ b/apps/client/src/features/workspace/types/workspace.types.ts @@ -35,6 +35,7 @@ export interface IInvitation { workspaceId: string; invitedById: string; createdAt: Date; + enforceSso: boolean; } export interface IInvitationLink { diff --git a/apps/server/src/core/auth/auth.util.ts b/apps/server/src/core/auth/auth.util.ts index ee781603..cdd46e9b 100644 --- a/apps/server/src/core/auth/auth.util.ts +++ b/apps/server/src/core/auth/auth.util.ts @@ -6,3 +6,16 @@ export function validateSsoEnforcement(workspace: Workspace) { throw new BadRequestException('This workspace has enforced SSO login.'); } } + +export function validateAllowedEmail(userEmail: string, workspace: Workspace) { + const emailParts = userEmail.split('@'); + const emailDomain = emailParts[1].toLowerCase(); + if ( + workspace.emailDomains?.length > 0 && + !workspace.emailDomains.includes(emailDomain) + ) { + throw new BadRequestException( + `The email domain "${emailDomain}" is not approved for this workspace.`, + ); + } +} diff --git a/apps/server/src/core/workspace/controllers/workspace.controller.ts b/apps/server/src/core/workspace/controllers/workspace.controller.ts index 218c79a3..0a8e3b0c 100644 --- a/apps/server/src/core/workspace/controllers/workspace.controller.ts +++ b/apps/server/src/core/workspace/controllers/workspace.controller.ts @@ -180,10 +180,13 @@ export class WorkspaceController { @Public() @HttpCode(HttpStatus.OK) @Post('invites/info') - async getInvitationById(@Body() dto: InvitationIdDto, @Req() req: any) { + async getInvitationById( + @Body() dto: InvitationIdDto, + @AuthWorkspace() workspace: Workspace, + ) { return this.workspaceInvitationService.getInvitationById( dto.invitationId, - req.raw.workspaceId, + workspace, ); } @@ -253,12 +256,12 @@ export class WorkspaceController { @Post('invites/accept') async acceptInvite( @Body() acceptInviteDto: AcceptInviteDto, - @Req() req: any, + @AuthWorkspace() workspace: Workspace, @Res({ passthrough: true }) res: FastifyReply, ) { const authToken = await this.workspaceInvitationService.acceptInvitation( acceptInviteDto, - req.raw.workspaceId, + workspace, ); res.setCookie('authToken', authToken, { diff --git a/apps/server/src/core/workspace/services/workspace-invitation.service.ts b/apps/server/src/core/workspace/services/workspace-invitation.service.ts index 9b22a048..a24a87ed 100644 --- a/apps/server/src/core/workspace/services/workspace-invitation.service.ts +++ b/apps/server/src/core/workspace/services/workspace-invitation.service.ts @@ -28,6 +28,10 @@ import { InjectQueue } from '@nestjs/bullmq'; import { QueueJob, QueueName } from '../../../integrations/queue/constants'; import { Queue } from 'bullmq'; import { EnvironmentService } from '../../../integrations/environment/environment.service'; +import { + validateAllowedEmail, + validateSsoEnforcement, +} from '../../auth/auth.util'; @Injectable() export class WorkspaceInvitationService { @@ -63,19 +67,19 @@ export class WorkspaceInvitationService { return result; } - async getInvitationById(invitationId: string, workspaceId: string) { + async getInvitationById(invitationId: string, workspace: Workspace) { const invitation = await this.db .selectFrom('workspaceInvitations') .select(['id', 'email', 'createdAt']) .where('id', '=', invitationId) - .where('workspaceId', '=', workspaceId) + .where('workspaceId', '=', workspace.id) .executeTakeFirst(); if (!invitation) { throw new NotFoundException('Invitation not found'); } - return invitation; + return { ...invitation, enforceSso: workspace.enforceSso }; } async getInvitationTokenById(invitationId: string, workspaceId: string) { @@ -169,12 +173,12 @@ export class WorkspaceInvitationService { } } - async acceptInvitation(dto: AcceptInviteDto, workspaceId: string) { + async acceptInvitation(dto: AcceptInviteDto, workspace: Workspace) { const invitation = await this.db .selectFrom('workspaceInvitations') .selectAll() .where('id', '=', dto.invitationId) - .where('workspaceId', '=', workspaceId) + .where('workspaceId', '=', workspace.id) .executeTakeFirst(); if (!invitation) { @@ -185,6 +189,9 @@ export class WorkspaceInvitationService { throw new BadRequestException('Invalid invitation token'); } + validateSsoEnforcement(workspace); + validateAllowedEmail(invitation.email, workspace); + let newUser: User; try { @@ -197,7 +204,7 @@ export class WorkspaceInvitationService { password: dto.password, role: invitation.role, invitedById: invitation.invitedById, - workspaceId: workspaceId, + workspaceId: workspace.id, }, trx, ); @@ -205,7 +212,7 @@ export class WorkspaceInvitationService { // add user to default group await this.groupUserRepo.addUserToDefaultGroup( newUser.id, - workspaceId, + workspace.id, trx, ); @@ -215,7 +222,7 @@ export class WorkspaceInvitationService { .selectFrom('groups') .select(['id', 'name']) .where('groups.id', 'in', invitation.groupIds) - .where('groups.workspaceId', '=', workspaceId) + .where('groups.workspaceId', '=', workspace.id) .execute(); if (validGroups && validGroups.length > 0) { @@ -256,7 +263,7 @@ export class WorkspaceInvitationService { // notify the inviter const invitedByUser = await this.userRepo.findById( invitation.invitedById, - workspaceId, + workspace.id, ); if (invitedByUser) { @@ -273,7 +280,9 @@ export class WorkspaceInvitationService { } if (this.environmentService.isCloud()) { - await this.billingQueue.add(QueueJob.STRIPE_SEATS_SYNC, { workspaceId }); + await this.billingQueue.add(QueueJob.STRIPE_SEATS_SYNC, { + workspaceId: workspace.id, + }); } return this.tokenService.generateAccessToken(newUser); diff --git a/apps/server/src/ee b/apps/server/src/ee index 70eb45ea..4d1b0a17 160000 --- a/apps/server/src/ee +++ b/apps/server/src/ee @@ -1 +1 @@ -Subproject commit 70eb45eaec84f61cb94a83a153915ce443ccc437 +Subproject commit 4d1b0a17d3be96d4f39bef6780d88b6c7fe49df9