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")}
-
-
+
+ )}
);
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