Fix user signup event (#12700)

Send event for every type of user creation
This commit is contained in:
Félix Malfait
2025-06-18 15:32:46 +02:00
committed by GitHub
parent 2ebe437885
commit 56d934872d
7 changed files with 59 additions and 83 deletions

View File

@ -25,16 +25,21 @@ import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { AvailableWorkspacesAndAccessTokensOutput } from 'src/engine/core-modules/auth/dto/available-workspaces-and-access-tokens.output';
import { GetAuthorizationUrlForSSOInput } from 'src/engine/core-modules/auth/dto/get-authorization-url-for-sso.input';
import { GetAuthorizationUrlForSSOOutput } from 'src/engine/core-modules/auth/dto/get-authorization-url-for-sso.output';
import { GetLoginTokenFromEmailVerificationTokenInput } from 'src/engine/core-modules/auth/dto/get-login-token-from-email-verification-token.input';
import { GetLoginTokenFromEmailVerificationTokenOutput } from 'src/engine/core-modules/auth/dto/get-login-token-from-email-verification-token.output';
import { SignUpOutput } from 'src/engine/core-modules/auth/dto/sign-up.output';
import { ResetPasswordService } from 'src/engine/core-modules/auth/services/reset-password.service';
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
import { EmailVerificationTokenService } from 'src/engine/core-modules/auth/token/services/email-verification-token.service';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service';
import { RenewTokenService } from 'src/engine/core-modules/auth/token/services/renew-token.service';
import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service';
import { WorkspaceAgnosticTokenService } from 'src/engine/core-modules/auth/token/services/workspace-agnostic-token.service';
import { JwtTokenTypeEnum } from 'src/engine/core-modules/auth/types/auth-context.type';
import { CaptchaGuard } from 'src/engine/core-modules/captcha/captcha.guard';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EmailVerificationExceptionFilter } from 'src/engine/core-modules/email-verification/email-verification-exception-filter.util';
@ -44,8 +49,10 @@ import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
import { AuthProvider } from 'src/engine/decorators/auth/auth-provider.decorator';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
@ -54,19 +61,12 @@ import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
import { GetLoginTokenFromEmailVerificationTokenOutput } from 'src/engine/core-modules/auth/dto/get-login-token-from-email-verification-token.output';
import { WorkspaceAgnosticTokenService } from 'src/engine/core-modules/auth/token/services/workspace-agnostic-token.service';
import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service';
import { AvailableWorkspacesAndAccessTokensOutput } from 'src/engine/core-modules/auth/dto/available-workspaces-and-access-tokens.output';
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
import { AuthProvider } from 'src/engine/decorators/auth/auth-provider.decorator';
import { JwtTokenTypeEnum } from 'src/engine/core-modules/auth/types/auth-context.type';
import { GetAuthTokensFromLoginTokenInput } from './dto/get-auth-tokens-from-login-token.input';
import { UserCredentialsInput } from './dto/user-credentials.input';
import { LoginToken } from './dto/login-token.entity';
import { SignUpInput } from './dto/sign-up.input';
import { ApiKeyToken, AuthTokens } from './dto/token.entity';
import { UserCredentialsInput } from './dto/user-credentials.input';
import { CheckUserExistOutput } from './dto/user-exists.entity';
import { EmailAndCaptchaInput } from './dto/user-exists.input';
import { WorkspaceInviteHashValid } from './dto/workspace-invite-hash-valid.entity';

View File

@ -36,10 +36,10 @@ import {
WorkspaceSSOIdentityProvider,
} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
import { User } from 'src/engine/core-modules/user/user.entity';
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
@Controller('auth')
export class SSOAuthController {

View File

@ -30,14 +30,20 @@ import {
} from 'src/engine/core-modules/auth/auth.util';
import { AuthorizeApp } from 'src/engine/core-modules/auth/dto/authorize-app.entity';
import { AuthorizeAppInput } from 'src/engine/core-modules/auth/dto/authorize-app.input';
import { UserCredentialsInput } from 'src/engine/core-modules/auth/dto/user-credentials.input';
import { AuthTokens } from 'src/engine/core-modules/auth/dto/token.entity';
import { UpdatePassword } from 'src/engine/core-modules/auth/dto/update-password.entity';
import { UserCredentialsInput } from 'src/engine/core-modules/auth/dto/user-credentials.input';
import { CheckUserExistOutput } from 'src/engine/core-modules/auth/dto/user-exists.entity';
import { WorkspaceInviteHashValid } from 'src/engine/core-modules/auth/dto/workspace-invite-hash-valid.entity';
import { AuthSsoService } from 'src/engine/core-modules/auth/services/auth-sso.service';
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
import { GoogleRequest } from 'src/engine/core-modules/auth/strategies/google.auth.strategy';
import { MicrosoftRequest } from 'src/engine/core-modules/auth/strategies/microsoft.auth.strategy';
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service';
import { WorkspaceAgnosticTokenService } from 'src/engine/core-modules/auth/token/services/workspace-agnostic-token.service';
import { JwtTokenTypeEnum } from 'src/engine/core-modules/auth/types/auth-context.type';
import {
AuthProviderWithPasswordType,
ExistingUserOrNewUser,
@ -47,6 +53,7 @@ import {
import { WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType } from 'src/engine/core-modules/domain-manager/domain-manager.type';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
@ -56,13 +63,6 @@ import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-in
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
import { CheckUserExistOutput } from 'src/engine/core-modules/auth/dto/user-exists.entity';
import { MicrosoftRequest } from 'src/engine/core-modules/auth/strategies/microsoft.auth.strategy';
import { GoogleRequest } from 'src/engine/core-modules/auth/strategies/google.auth.strategy';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
import { WorkspaceAgnosticTokenService } from 'src/engine/core-modules/auth/token/services/workspace-agnostic-token.service';
import { JwtTokenTypeEnum } from 'src/engine/core-modules/auth/types/auth-context.type';
@Injectable()
// eslint-disable-next-line @nx/workspace-inject-workspace-repository

View File

@ -5,13 +5,13 @@ import { getRepositoryToken } from '@nestjs/typeorm';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { Repository } from 'typeorm';
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import {
AuthProviderWithPasswordType,
ExistingUserOrPartialUserWithPicture,
@ -27,9 +27,10 @@ import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/use
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
jest.mock('src/utils/image', () => {
return {
@ -136,6 +137,12 @@ describe('SignInUpService', () => {
isFeatureEnabled: jest.fn(),
},
},
{
provide: WorkspaceEventEmitter,
useValue: {
emitCustomBatchEvent: jest.fn(),
},
},
],
}).compile();

View File

@ -7,6 +7,7 @@ import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import { USER_SIGNUP_EVENT_NAME } from 'src/engine/api/graphql/workspace-query-runner/constants/user-signup-event-name.constants';
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
import {
AuthException,
@ -31,11 +32,11 @@ import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/use
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
import { getDomainNameByEmail } from 'src/utils/get-domain-name-by-email';
import { isWorkEmail } from 'src/utils/is-work-email';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
@Injectable()
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
@ -48,7 +49,7 @@ export class SignInUpService {
private readonly workspaceInvitationService: WorkspaceInvitationService,
private readonly userWorkspaceService: UserWorkspaceService,
private readonly onboardingService: OnboardingService,
private readonly loginTokenService: LoginTokenService,
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
private readonly httpService: HttpService,
private readonly twentyConfigService: TwentyConfigService,
private readonly domainManagerService: DomainManagerService,
@ -287,7 +288,26 @@ export class SignInUpService {
canAccessFullAdminPanel,
});
return await this.userRepository.save(userCreated);
const savedUser = await this.userRepository.save(userCreated);
const serverUrl = this.twentyConfigService.get('SERVER_URL');
this.workspaceEventEmitter.emitCustomBatchEvent(
USER_SIGNUP_EVENT_NAME,
[
{
userId: savedUser.id,
userEmail: newUserWithPicture.email,
userFirstName: newUserWithPicture.firstName,
userLastName: newUserWithPicture.lastName,
locale: newUserWithPicture.locale,
serverUrl,
},
],
undefined,
);
return savedUser;
}
private async setDefaultImpersonateAndAccessFullAdminPanel() {

View File

@ -1,14 +1,16 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { DataSource, IsNull, Not, Repository } from 'typeorm';
import { DataSource, Repository } from 'typeorm';
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { USER_SIGNUP_EVENT_NAME } from 'src/engine/api/graphql/workspace-query-runner/constants/user-signup-event-name.constants';
import { ApprovedAccessDomain } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.entity';
import { ApprovedAccessDomainService } from 'src/engine/core-modules/approved-access-domain/services/approved-access-domain.service';
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import {
@ -28,9 +30,6 @@ import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { ApprovedAccessDomainService } from 'src/engine/core-modules/approved-access-domain/services/approved-access-domain.service';
import { ApprovedAccessDomain } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.entity';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
describe('UserWorkspaceService', () => {
let service: UserWorkspaceService;
@ -198,9 +197,6 @@ describe('UserWorkspaceService', () => {
jest
.spyOn(fileService, 'copyFileFromWorkspaceToWorkspace')
.mockResolvedValue(['', 'path/to', 'copy']);
jest
.spyOn(workspaceEventEmitter, 'emitCustomBatchEvent')
.mockImplementation();
const result = await service.create({
userId,
@ -208,16 +204,6 @@ describe('UserWorkspaceService', () => {
isExistingUser: true,
});
expect(userWorkspaceRepository.findOne).toHaveBeenCalledWith({
where: {
userId,
defaultAvatarUrl: Not(IsNull()),
},
order: {
createdAt: 'ASC',
},
});
expect(userWorkspaceRepository.create).toHaveBeenCalledWith({
userId,
workspaceId,
@ -225,11 +211,6 @@ describe('UserWorkspaceService', () => {
});
expect(userWorkspaceRepository.save).toHaveBeenCalledWith(userWorkspace);
expect(workspaceEventEmitter.emitCustomBatchEvent).toHaveBeenCalledWith(
USER_SIGNUP_EVENT_NAME,
[{ userId }],
workspaceId,
);
expect(result).toEqual(userWorkspace);
});
it("should create a user workspace without a default avatar url if it's an existing user without any user workspace having a default avatar url", async () => {
@ -246,10 +227,6 @@ describe('UserWorkspaceService', () => {
jest.spyOn(userWorkspaceRepository, 'findOne').mockResolvedValue(null);
jest
.spyOn(workspaceEventEmitter, 'emitCustomBatchEvent')
.mockImplementation();
const result = await service.create({
userId,
workspaceId,
@ -262,11 +239,6 @@ describe('UserWorkspaceService', () => {
defaultAvatarUrl: undefined,
});
expect(userWorkspaceRepository.save).toHaveBeenCalledWith(userWorkspace);
expect(workspaceEventEmitter.emitCustomBatchEvent).toHaveBeenCalledWith(
USER_SIGNUP_EVENT_NAME,
[{ userId }],
workspaceId,
);
expect(result).toEqual(userWorkspace);
});
it("should create a user workspace with a default avatar url if it's a new user with a picture url", async () => {
@ -281,9 +253,6 @@ describe('UserWorkspaceService', () => {
.spyOn(userWorkspaceRepository, 'save')
.mockResolvedValue(userWorkspace);
jest
.spyOn(workspaceEventEmitter, 'emitCustomBatchEvent')
.mockImplementation();
jest.spyOn(fileUploadService, 'uploadImageFromUrl').mockResolvedValue({
files: [{ path: 'path/to/file', token: 'token' }],
} as SignedFilesResult);
@ -306,11 +275,6 @@ describe('UserWorkspaceService', () => {
defaultAvatarUrl: 'path/to/file',
});
expect(userWorkspaceRepository.save).toHaveBeenCalledWith(userWorkspace);
expect(workspaceEventEmitter.emitCustomBatchEvent).toHaveBeenCalledWith(
USER_SIGNUP_EVENT_NAME,
[{ userId }],
workspaceId,
);
expect(result).toEqual(userWorkspace);
});
it("should create a user workspace without a default avatar url if it's a new user without a picture url", async () => {
@ -324,9 +288,6 @@ describe('UserWorkspaceService', () => {
jest
.spyOn(userWorkspaceRepository, 'save')
.mockResolvedValue(userWorkspace);
jest
.spyOn(workspaceEventEmitter, 'emitCustomBatchEvent')
.mockImplementation();
const result = await service.create({
userId,
@ -341,11 +302,6 @@ describe('UserWorkspaceService', () => {
defaultAvatarUrl: undefined,
});
expect(userWorkspaceRepository.save).toHaveBeenCalledWith(userWorkspace);
expect(workspaceEventEmitter.emitCustomBatchEvent).toHaveBeenCalledWith(
USER_SIGNUP_EVENT_NAME,
[{ userId }],
workspaceId,
);
expect(result).toEqual(userWorkspace);
});
});

View File

@ -9,17 +9,21 @@ import { IsNull, Not, Repository } from 'typeorm';
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { USER_SIGNUP_EVENT_NAME } from 'src/engine/api/graphql/workspace-query-runner/constants/user-signup-event-name.constants';
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
import { ApprovedAccessDomainService } from 'src/engine/core-modules/approved-access-domain/services/approved-access-domain.service';
import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { AvailableWorkspace } from 'src/engine/core-modules/auth/dto/available-workspaces.output';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { User } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@ -33,12 +37,7 @@ import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { assert } from 'src/utils/assert';
import { ApprovedAccessDomainService } from 'src/engine/core-modules/approved-access-domain/services/approved-access-domain.service';
import { getDomainNameByEmail } from 'src/utils/get-domain-name-by-email';
import { AvailableWorkspace } from 'src/engine/core-modules/auth/dto/available-workspaces.output';
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
constructor(
@ -85,12 +84,6 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
defaultAvatarUrl,
});
this.workspaceEventEmitter.emitCustomBatchEvent(
USER_SIGNUP_EVENT_NAME,
[{ userId }],
workspaceId,
);
return this.userWorkspaceRepository.save(userWorkspace);
}