feat(auth): enhance email validation when no workspace available + disable captcha on email validation (#11239)

Implemented fallback logic to associate a user with a workspace when
none is found. Introduced new GraphQL types and mutations for roles and
permissions management. Simplified and refactored URL-building logic for
email verification, improving code maintainability and flexibility.
This commit is contained in:
Antoine Moreaux
2025-03-28 07:38:58 +01:00
committed by GitHub
parent 976c6afb4b
commit 9af2628264
5 changed files with 69 additions and 43 deletions

View File

@ -155,31 +155,25 @@ export class AuthResolver {
return { loginToken };
}
@UseGuards(CaptchaGuard)
@Mutation(() => LoginToken)
async getLoginTokenFromEmailVerificationToken(
@Args()
getLoginTokenFromEmailVerificationTokenInput: GetLoginTokenFromEmailVerificationTokenInput,
@OriginHeader() origin: string,
) {
const workspace =
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
origin,
);
workspaceValidator.assertIsDefinedOrThrow(
workspace,
new AuthException(
'Workspace not found',
AuthExceptionCode.WORKSPACE_NOT_FOUND,
),
);
const user =
await this.emailVerificationTokenService.validateEmailVerificationTokenOrThrow(
getLoginTokenFromEmailVerificationTokenInput.emailVerificationToken,
);
const workspace =
(await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
origin,
)) ??
(await this.userWorkspaceService.findFirstRandomWorkspaceByUserId(
user.id,
));
await this.userService.markEmailAsVerified(user.id);
const loginToken = await this.loginTokenService.generateLoginToken(

View File

@ -42,22 +42,37 @@ export class DomainManagerService {
return baseUrl;
}
buildEmailVerificationURL({
emailVerificationToken,
email,
workspace,
}: {
emailVerificationToken: string;
email: string;
workspace: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType;
}) {
return this.buildWorkspaceURL({
workspace,
pathname: 'verify-email',
searchParams: { emailVerificationToken, email },
private appendSearchParams(
url: URL,
searchParams: Record<string, string | number>,
) {
Object.entries(searchParams).forEach(([key, value]) => {
if (isDefined(value)) {
url.searchParams.set(key, value.toString());
}
});
}
buildBaseUrl({
pathname,
searchParams,
}: {
pathname?: string;
searchParams?: Record<string, string | number>;
}) {
const url = this.getBaseUrl();
if (pathname) {
url.pathname = pathname;
}
if (searchParams) {
this.appendSearchParams(url, searchParams);
}
return url;
}
buildWorkspaceURL({
workspace,
pathname,
@ -76,11 +91,7 @@ export class DomainManagerService {
}
if (searchParams) {
Object.entries(searchParams).forEach(([key, value]) => {
if (isDefined(value)) {
url.searchParams.set(key, value.toString());
}
});
this.appendSearchParams(url, searchParams);
}
return url;

View File

@ -7,7 +7,6 @@ import { ResendEmailVerificationTokenInput } from 'src/engine/core-modules/email
import { ResendEmailVerificationTokenOutput } from 'src/engine/core-modules/email-verification/dtos/resend-email-verification-token.output';
import { EmailVerificationService } from 'src/engine/core-modules/email-verification/services/email-verification.service';
import { I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type';
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator';
@Resolver()
@ -29,8 +28,6 @@ export class EmailVerificationResolver {
origin,
);
workspaceValidator.assertIsDefinedOrThrow(workspace);
return await this.emailVerificationService.resendEmailVerificationToken(
resendEmailVerificationTokenInput.email,
workspace,

View File

@ -41,7 +41,9 @@ export class EmailVerificationService {
async sendVerificationEmail(
userId: string,
email: string,
workspace: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType,
workspace:
| WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType
| undefined,
locale: keyof typeof APP_LOCALES,
) {
if (!this.environmentService.get('IS_EMAIL_VERIFICATION_REQUIRED')) {
@ -51,12 +53,16 @@ export class EmailVerificationService {
const { token: emailVerificationToken } =
await this.emailVerificationTokenService.generateToken(userId, email);
const verificationLink =
this.domainManagerService.buildEmailVerificationURL({
emailVerificationToken,
email,
workspace,
});
const linkPathnameAndSearchParams = {
pathname: 'verify-email',
searchParams: { emailVerificationToken, email },
};
const verificationLink = workspace
? this.domainManagerService.buildWorkspaceURL({
workspace,
...linkPathnameAndSearchParams,
})
: this.domainManagerService.buildBaseUrl(linkPathnameAndSearchParams);
const emailData = {
link: verificationLink.toString(),
@ -88,7 +94,9 @@ export class EmailVerificationService {
async resendEmailVerificationToken(
email: string,
workspace: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType,
workspace:
| WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType
| undefined,
locale: keyof typeof APP_LOCALES,
) {
if (!this.environmentService.get('IS_EMAIL_VERIFICATION_REQUIRED')) {

View File

@ -186,6 +186,22 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
});
}
async findFirstRandomWorkspaceByUserId(userId: string) {
const user = await this.userRepository.findOne({
where: {
id: userId,
},
relations: ['workspaces', 'workspaces.workspace'],
});
userValidator.assertIsDefinedOrThrow(
user,
new AuthException('User not found', AuthExceptionCode.USER_NOT_FOUND),
);
return user.workspaces[0].workspace;
}
async findAvailableWorkspacesByEmail(email: string) {
const user = await this.userRepository.findOne({
where: {