mirror of
https://github.com/docmost/docmost.git
synced 2026-05-17 23:14:07 +08:00
fix: enforce SSO in invitation signups (#1258)
This commit is contained in:
@@ -18,6 +18,7 @@ import classes from "@/features/auth/components/auth.module.css";
|
|||||||
import { useGetInvitationQuery } from "@/features/workspace/queries/workspace-query.ts";
|
import { useGetInvitationQuery } from "@/features/workspace/queries/workspace-query.ts";
|
||||||
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
|
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import SsoLogin from "@/ee/components/sso-login.tsx";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
name: z.string().trim().min(1),
|
name: z.string().trim().min(1),
|
||||||
@@ -71,39 +72,43 @@ export function InviteSignUpForm() {
|
|||||||
{t("Join the workspace")}
|
{t("Join the workspace")}
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<Stack align="stretch" justify="center" gap="xl">
|
<SsoLogin />
|
||||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
|
||||||
<TextInput
|
|
||||||
id="name"
|
|
||||||
type="text"
|
|
||||||
label={t("Name")}
|
|
||||||
placeholder={t("enter your full name")}
|
|
||||||
variant="filled"
|
|
||||||
{...form.getInputProps("name")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextInput
|
{!invitation.enforceSso && (
|
||||||
id="email"
|
<Stack align="stretch" justify="center" gap="xl">
|
||||||
type="email"
|
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||||
label={t("Email")}
|
<TextInput
|
||||||
value={invitation.email}
|
id="name"
|
||||||
disabled
|
type="text"
|
||||||
variant="filled"
|
label={t("Name")}
|
||||||
mt="md"
|
placeholder={t("enter your full name")}
|
||||||
/>
|
variant="filled"
|
||||||
|
{...form.getInputProps("name")}
|
||||||
|
/>
|
||||||
|
|
||||||
<PasswordInput
|
<TextInput
|
||||||
label={t("Password")}
|
id="email"
|
||||||
placeholder={t("Your password")}
|
type="email"
|
||||||
variant="filled"
|
label={t("Email")}
|
||||||
mt="md"
|
value={invitation.email}
|
||||||
{...form.getInputProps("password")}
|
disabled
|
||||||
/>
|
variant="filled"
|
||||||
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
|
mt="md"
|
||||||
{t("Sign Up")}
|
/>
|
||||||
</Button>
|
|
||||||
</form>
|
<PasswordInput
|
||||||
</Stack>
|
label={t("Password")}
|
||||||
|
placeholder={t("Your password")}
|
||||||
|
variant="filled"
|
||||||
|
mt="md"
|
||||||
|
{...form.getInputProps("password")}
|
||||||
|
/>
|
||||||
|
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
|
||||||
|
{t("Sign Up")}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ export function useRevokeInvitationMutation() {
|
|||||||
|
|
||||||
export function useGetInvitationQuery(
|
export function useGetInvitationQuery(
|
||||||
invitationId: string,
|
invitationId: string,
|
||||||
): UseQueryResult<any, Error> {
|
): UseQueryResult<IInvitation, Error> {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["invitations", invitationId],
|
queryKey: ["invitations", invitationId],
|
||||||
queryFn: () => getInvitationById({ invitationId }),
|
queryFn: () => getInvitationById({ invitationId }),
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export interface IInvitation {
|
|||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
invitedById: string;
|
invitedById: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
enforceSso: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IInvitationLink {
|
export interface IInvitationLink {
|
||||||
|
|||||||
@@ -6,3 +6,16 @@ export function validateSsoEnforcement(workspace: Workspace) {
|
|||||||
throw new BadRequestException('This workspace has enforced SSO login.');
|
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.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -180,10 +180,13 @@ export class WorkspaceController {
|
|||||||
@Public()
|
@Public()
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('invites/info')
|
@Post('invites/info')
|
||||||
async getInvitationById(@Body() dto: InvitationIdDto, @Req() req: any) {
|
async getInvitationById(
|
||||||
|
@Body() dto: InvitationIdDto,
|
||||||
|
@AuthWorkspace() workspace: Workspace,
|
||||||
|
) {
|
||||||
return this.workspaceInvitationService.getInvitationById(
|
return this.workspaceInvitationService.getInvitationById(
|
||||||
dto.invitationId,
|
dto.invitationId,
|
||||||
req.raw.workspaceId,
|
workspace,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,12 +256,12 @@ export class WorkspaceController {
|
|||||||
@Post('invites/accept')
|
@Post('invites/accept')
|
||||||
async acceptInvite(
|
async acceptInvite(
|
||||||
@Body() acceptInviteDto: AcceptInviteDto,
|
@Body() acceptInviteDto: AcceptInviteDto,
|
||||||
@Req() req: any,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
@Res({ passthrough: true }) res: FastifyReply,
|
@Res({ passthrough: true }) res: FastifyReply,
|
||||||
) {
|
) {
|
||||||
const authToken = await this.workspaceInvitationService.acceptInvitation(
|
const authToken = await this.workspaceInvitationService.acceptInvitation(
|
||||||
acceptInviteDto,
|
acceptInviteDto,
|
||||||
req.raw.workspaceId,
|
workspace,
|
||||||
);
|
);
|
||||||
|
|
||||||
res.setCookie('authToken', authToken, {
|
res.setCookie('authToken', authToken, {
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ import { InjectQueue } from '@nestjs/bullmq';
|
|||||||
import { QueueJob, QueueName } from '../../../integrations/queue/constants';
|
import { QueueJob, QueueName } from '../../../integrations/queue/constants';
|
||||||
import { Queue } from 'bullmq';
|
import { Queue } from 'bullmq';
|
||||||
import { EnvironmentService } from '../../../integrations/environment/environment.service';
|
import { EnvironmentService } from '../../../integrations/environment/environment.service';
|
||||||
|
import {
|
||||||
|
validateAllowedEmail,
|
||||||
|
validateSsoEnforcement,
|
||||||
|
} from '../../auth/auth.util';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WorkspaceInvitationService {
|
export class WorkspaceInvitationService {
|
||||||
@@ -63,19 +67,19 @@ export class WorkspaceInvitationService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getInvitationById(invitationId: string, workspaceId: string) {
|
async getInvitationById(invitationId: string, workspace: Workspace) {
|
||||||
const invitation = await this.db
|
const invitation = await this.db
|
||||||
.selectFrom('workspaceInvitations')
|
.selectFrom('workspaceInvitations')
|
||||||
.select(['id', 'email', 'createdAt'])
|
.select(['id', 'email', 'createdAt'])
|
||||||
.where('id', '=', invitationId)
|
.where('id', '=', invitationId)
|
||||||
.where('workspaceId', '=', workspaceId)
|
.where('workspaceId', '=', workspace.id)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|
||||||
if (!invitation) {
|
if (!invitation) {
|
||||||
throw new NotFoundException('Invitation not found');
|
throw new NotFoundException('Invitation not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return invitation;
|
return { ...invitation, enforceSso: workspace.enforceSso };
|
||||||
}
|
}
|
||||||
|
|
||||||
async getInvitationTokenById(invitationId: string, workspaceId: string) {
|
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
|
const invitation = await this.db
|
||||||
.selectFrom('workspaceInvitations')
|
.selectFrom('workspaceInvitations')
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.where('id', '=', dto.invitationId)
|
.where('id', '=', dto.invitationId)
|
||||||
.where('workspaceId', '=', workspaceId)
|
.where('workspaceId', '=', workspace.id)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|
||||||
if (!invitation) {
|
if (!invitation) {
|
||||||
@@ -185,6 +189,9 @@ export class WorkspaceInvitationService {
|
|||||||
throw new BadRequestException('Invalid invitation token');
|
throw new BadRequestException('Invalid invitation token');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateSsoEnforcement(workspace);
|
||||||
|
validateAllowedEmail(invitation.email, workspace);
|
||||||
|
|
||||||
let newUser: User;
|
let newUser: User;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -197,7 +204,7 @@ export class WorkspaceInvitationService {
|
|||||||
password: dto.password,
|
password: dto.password,
|
||||||
role: invitation.role,
|
role: invitation.role,
|
||||||
invitedById: invitation.invitedById,
|
invitedById: invitation.invitedById,
|
||||||
workspaceId: workspaceId,
|
workspaceId: workspace.id,
|
||||||
},
|
},
|
||||||
trx,
|
trx,
|
||||||
);
|
);
|
||||||
@@ -205,7 +212,7 @@ export class WorkspaceInvitationService {
|
|||||||
// add user to default group
|
// add user to default group
|
||||||
await this.groupUserRepo.addUserToDefaultGroup(
|
await this.groupUserRepo.addUserToDefaultGroup(
|
||||||
newUser.id,
|
newUser.id,
|
||||||
workspaceId,
|
workspace.id,
|
||||||
trx,
|
trx,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -215,7 +222,7 @@ export class WorkspaceInvitationService {
|
|||||||
.selectFrom('groups')
|
.selectFrom('groups')
|
||||||
.select(['id', 'name'])
|
.select(['id', 'name'])
|
||||||
.where('groups.id', 'in', invitation.groupIds)
|
.where('groups.id', 'in', invitation.groupIds)
|
||||||
.where('groups.workspaceId', '=', workspaceId)
|
.where('groups.workspaceId', '=', workspace.id)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
if (validGroups && validGroups.length > 0) {
|
if (validGroups && validGroups.length > 0) {
|
||||||
@@ -256,7 +263,7 @@ export class WorkspaceInvitationService {
|
|||||||
// notify the inviter
|
// notify the inviter
|
||||||
const invitedByUser = await this.userRepo.findById(
|
const invitedByUser = await this.userRepo.findById(
|
||||||
invitation.invitedById,
|
invitation.invitedById,
|
||||||
workspaceId,
|
workspace.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (invitedByUser) {
|
if (invitedByUser) {
|
||||||
@@ -273,7 +280,9 @@ export class WorkspaceInvitationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.environmentService.isCloud()) {
|
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);
|
return this.tokenService.generateAccessToken(newUser);
|
||||||
|
|||||||
+1
-1
Submodule apps/server/src/ee updated: 70eb45eaec...4d1b0a17d3
Reference in New Issue
Block a user