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:
@ -155,31 +155,25 @@ export class AuthResolver {
|
|||||||
return { loginToken };
|
return { loginToken };
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(CaptchaGuard)
|
|
||||||
@Mutation(() => LoginToken)
|
@Mutation(() => LoginToken)
|
||||||
async getLoginTokenFromEmailVerificationToken(
|
async getLoginTokenFromEmailVerificationToken(
|
||||||
@Args()
|
@Args()
|
||||||
getLoginTokenFromEmailVerificationTokenInput: GetLoginTokenFromEmailVerificationTokenInput,
|
getLoginTokenFromEmailVerificationTokenInput: GetLoginTokenFromEmailVerificationTokenInput,
|
||||||
@OriginHeader() origin: string,
|
@OriginHeader() origin: string,
|
||||||
) {
|
) {
|
||||||
const workspace =
|
|
||||||
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
|
|
||||||
origin,
|
|
||||||
);
|
|
||||||
|
|
||||||
workspaceValidator.assertIsDefinedOrThrow(
|
|
||||||
workspace,
|
|
||||||
new AuthException(
|
|
||||||
'Workspace not found',
|
|
||||||
AuthExceptionCode.WORKSPACE_NOT_FOUND,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const user =
|
const user =
|
||||||
await this.emailVerificationTokenService.validateEmailVerificationTokenOrThrow(
|
await this.emailVerificationTokenService.validateEmailVerificationTokenOrThrow(
|
||||||
getLoginTokenFromEmailVerificationTokenInput.emailVerificationToken,
|
getLoginTokenFromEmailVerificationTokenInput.emailVerificationToken,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const workspace =
|
||||||
|
(await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
|
||||||
|
origin,
|
||||||
|
)) ??
|
||||||
|
(await this.userWorkspaceService.findFirstRandomWorkspaceByUserId(
|
||||||
|
user.id,
|
||||||
|
));
|
||||||
|
|
||||||
await this.userService.markEmailAsVerified(user.id);
|
await this.userService.markEmailAsVerified(user.id);
|
||||||
|
|
||||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||||
|
|||||||
@ -42,22 +42,37 @@ export class DomainManagerService {
|
|||||||
return baseUrl;
|
return baseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildEmailVerificationURL({
|
private appendSearchParams(
|
||||||
emailVerificationToken,
|
url: URL,
|
||||||
email,
|
searchParams: Record<string, string | number>,
|
||||||
workspace,
|
) {
|
||||||
}: {
|
Object.entries(searchParams).forEach(([key, value]) => {
|
||||||
emailVerificationToken: string;
|
if (isDefined(value)) {
|
||||||
email: string;
|
url.searchParams.set(key, value.toString());
|
||||||
workspace: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType;
|
}
|
||||||
}) {
|
|
||||||
return this.buildWorkspaceURL({
|
|
||||||
workspace,
|
|
||||||
pathname: 'verify-email',
|
|
||||||
searchParams: { emailVerificationToken, email },
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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({
|
buildWorkspaceURL({
|
||||||
workspace,
|
workspace,
|
||||||
pathname,
|
pathname,
|
||||||
@ -76,11 +91,7 @@ export class DomainManagerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (searchParams) {
|
if (searchParams) {
|
||||||
Object.entries(searchParams).forEach(([key, value]) => {
|
this.appendSearchParams(url, searchParams);
|
||||||
if (isDefined(value)) {
|
|
||||||
url.searchParams.set(key, value.toString());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
|
|||||||
@ -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 { 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 { 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 { 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';
|
import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator';
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
@ -29,8 +28,6 @@ export class EmailVerificationResolver {
|
|||||||
origin,
|
origin,
|
||||||
);
|
);
|
||||||
|
|
||||||
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
|
||||||
|
|
||||||
return await this.emailVerificationService.resendEmailVerificationToken(
|
return await this.emailVerificationService.resendEmailVerificationToken(
|
||||||
resendEmailVerificationTokenInput.email,
|
resendEmailVerificationTokenInput.email,
|
||||||
workspace,
|
workspace,
|
||||||
|
|||||||
@ -41,7 +41,9 @@ export class EmailVerificationService {
|
|||||||
async sendVerificationEmail(
|
async sendVerificationEmail(
|
||||||
userId: string,
|
userId: string,
|
||||||
email: string,
|
email: string,
|
||||||
workspace: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType,
|
workspace:
|
||||||
|
| WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType
|
||||||
|
| undefined,
|
||||||
locale: keyof typeof APP_LOCALES,
|
locale: keyof typeof APP_LOCALES,
|
||||||
) {
|
) {
|
||||||
if (!this.environmentService.get('IS_EMAIL_VERIFICATION_REQUIRED')) {
|
if (!this.environmentService.get('IS_EMAIL_VERIFICATION_REQUIRED')) {
|
||||||
@ -51,12 +53,16 @@ export class EmailVerificationService {
|
|||||||
const { token: emailVerificationToken } =
|
const { token: emailVerificationToken } =
|
||||||
await this.emailVerificationTokenService.generateToken(userId, email);
|
await this.emailVerificationTokenService.generateToken(userId, email);
|
||||||
|
|
||||||
const verificationLink =
|
const linkPathnameAndSearchParams = {
|
||||||
this.domainManagerService.buildEmailVerificationURL({
|
pathname: 'verify-email',
|
||||||
emailVerificationToken,
|
searchParams: { emailVerificationToken, email },
|
||||||
email,
|
};
|
||||||
workspace,
|
const verificationLink = workspace
|
||||||
});
|
? this.domainManagerService.buildWorkspaceURL({
|
||||||
|
workspace,
|
||||||
|
...linkPathnameAndSearchParams,
|
||||||
|
})
|
||||||
|
: this.domainManagerService.buildBaseUrl(linkPathnameAndSearchParams);
|
||||||
|
|
||||||
const emailData = {
|
const emailData = {
|
||||||
link: verificationLink.toString(),
|
link: verificationLink.toString(),
|
||||||
@ -88,7 +94,9 @@ export class EmailVerificationService {
|
|||||||
|
|
||||||
async resendEmailVerificationToken(
|
async resendEmailVerificationToken(
|
||||||
email: string,
|
email: string,
|
||||||
workspace: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType,
|
workspace:
|
||||||
|
| WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType
|
||||||
|
| undefined,
|
||||||
locale: keyof typeof APP_LOCALES,
|
locale: keyof typeof APP_LOCALES,
|
||||||
) {
|
) {
|
||||||
if (!this.environmentService.get('IS_EMAIL_VERIFICATION_REQUIRED')) {
|
if (!this.environmentService.get('IS_EMAIL_VERIFICATION_REQUIRED')) {
|
||||||
|
|||||||
@ -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) {
|
async findAvailableWorkspacesByEmail(email: string) {
|
||||||
const user = await this.userRepository.findOne({
|
const user = await this.userRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
Reference in New Issue
Block a user