Fix authUser decorator usage (#12697)

Solving issue: we don't have `user.firstName` and `user.lastName` set
when signin with e-mail/password. CreateBy, invitation emails and
validation domain email need those info

## Before

## ExecutedBy

<img width="511" alt="image"
src="https://github.com/user-attachments/assets/b85bbda5-f26b-4137-a875-0ef926a1eec4"
/>

## Invitation email

<img width="764" alt="image"
src="https://github.com/user-attachments/assets/107c71bf-a6b2-4291-a31b-6ce48b11dd77"
/>

### Validate domain email

<img width="829" alt="image"
src="https://github.com/user-attachments/assets/213ff7c5-f86d-476f-8f4d-74299d7eb13d"
/>


## After

## ExecutedBy

<img width="500" alt="image"
src="https://github.com/user-attachments/assets/b4125e84-b355-4280-8611-b4e36e6033c7"
/>

## Invitation email

<img width="754" alt="image"
src="https://github.com/user-attachments/assets/952fe5bf-f4da-4fef-b765-fc220255dedf"
/>

### Validate domain email

<img width="709" alt="image"
src="https://github.com/user-attachments/assets/6950097c-51ae-469b-a7cf-f561650ee86e"
/>
This commit is contained in:
martmull
2025-06-18 15:57:55 +02:00
committed by GitHub
parent 82876bb7d1
commit d284fd1d71
7 changed files with 100 additions and 40 deletions

View File

@ -9,15 +9,18 @@ import { ValidateApprovedAccessDomainInput } from 'src/engine/core-modules/appro
import { ApprovedAccessDomainService } from 'src/engine/core-modules/approved-access-domain/services/approved-access-domain.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
@UseGuards(WorkspaceAuthGuard)
@UseFilters(ApprovedAccessDomainExceptionFilter)
@Resolver()
export class ApprovedAccessDomainResolver {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly approvedAccessDomainService: ApprovedAccessDomainService,
) {}
@ -27,10 +30,22 @@ export class ApprovedAccessDomainResolver {
@AuthWorkspace() currentWorkspace: Workspace,
@AuthUser() currentUser: User,
): Promise<ApprovedAccessDomain> {
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
currentWorkspace.id,
'workspaceMember',
);
const workspaceMember = await workspaceMemberRepository.findOneOrFail({
where: {
userId: currentUser.id,
},
});
return this.approvedAccessDomainService.createApprovedAccessDomain(
domain,
currentWorkspace,
currentUser,
workspaceMember,
email,
);
}

View File

@ -17,9 +17,9 @@ import { approvedAccessDomainValidator } from 'src/engine/core-modules/approved-
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { isWorkDomain } from 'src/utils/is-work-email';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Injectable()
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
@ -33,7 +33,7 @@ export class ApprovedAccessDomainService {
) {}
async sendApprovedAccessDomainValidationEmail(
sender: User,
sender: WorkspaceMemberWorkspaceEntity,
to: string,
workspace: Workspace,
approvedAccessDomain: ApprovedAccessDomainEntity,
@ -66,9 +66,9 @@ export class ApprovedAccessDomainService {
workspace: { name: workspace.displayName, logo: workspace.logo },
domain: approvedAccessDomain.domain,
sender: {
email: sender.email,
firstName: sender.firstName,
lastName: sender.lastName,
email: sender.userEmail,
firstName: sender.name.firstName,
lastName: sender.name.lastName,
},
serverUrl: this.twentyConfigService.get('SERVER_URL'),
locale: 'en' as keyof typeof APP_LOCALES,
@ -79,7 +79,7 @@ export class ApprovedAccessDomainService {
});
await this.emailService.send({
from: `${sender.firstName} ${sender.lastName} (via Twenty) <${this.twentyConfigService.get('EMAIL_FROM_ADDRESS')}>`,
from: `${sender.name.firstName} ${sender.name.lastName} (via Twenty) <${this.twentyConfigService.get('EMAIL_FROM_ADDRESS')}>`,
to,
subject: 'Approve your access domain',
text,
@ -140,7 +140,7 @@ export class ApprovedAccessDomainService {
async createApprovedAccessDomain(
domain: string,
inWorkspace: Workspace,
fromUser: User,
fromWorkspaceMember: WorkspaceMemberWorkspaceEntity,
emailToValidateDomain: string,
): Promise<ApprovedAccessDomainEntity> {
if (!isWorkDomain(domain)) {
@ -170,7 +170,7 @@ export class ApprovedAccessDomainService {
);
await this.sendApprovedAccessDomainValidationEmail(
fromUser,
fromWorkspaceMember,
emailToValidateDomain,
inWorkspace,
approvedAccessDomain,

View File

@ -11,8 +11,8 @@ import {
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { ApprovedAccessDomainService } from './approved-access-domain.service';
@ -78,9 +78,8 @@ describe('ApprovedAccessDomainService', () => {
isCustomDomainEnabled: false,
} as Workspace;
const fromUser = {
email: 'user@custom-domain.com',
isEmailVerified: true,
} as User;
userEmail: 'user@custom-domain.com',
} as WorkspaceMemberWorkspaceEntity;
const expectedApprovedAccessDomain = {
workspaceId: 'workspace-id',
@ -118,7 +117,9 @@ describe('ApprovedAccessDomainService', () => {
service.createApprovedAccessDomain(
'gmail.com',
{ id: 'workspace-id' } as Workspace,
{ email: 'user@gmail.com', isEmailVerified: true } as User,
{
userEmail: 'user@gmail.com',
} as WorkspaceMemberWorkspaceEntity,
'user@gmail.com',
),
).rejects.toThrowError(
@ -184,7 +185,7 @@ describe('ApprovedAccessDomainService', () => {
describe('sendApprovedAccessDomainValidationEmail', () => {
it('should throw an exception if the approved access domain is already validated', async () => {
const approvedAccessDomainId = 'approved-access-domain-id';
const sender = {} as User;
const sender = {} as WorkspaceMemberWorkspaceEntity;
const workspace = {} as Workspace;
const email = 'validator@example.com';
@ -214,7 +215,7 @@ describe('ApprovedAccessDomainService', () => {
it('should throw an exception if the email does not match the approved access domain', async () => {
const approvedAccessDomainId = 'approved-access-domain-id';
const sender = {} as User;
const sender = {} as WorkspaceMemberWorkspaceEntity;
const workspace = {} as Workspace;
const email = 'validator@different.com';
const approvedAccessDomain = {
@ -244,10 +245,9 @@ describe('ApprovedAccessDomainService', () => {
it('should send a validation email if all conditions are met', async () => {
const sender = {
email: 'sender@example.com',
firstName: 'John',
lastName: 'Doe',
} as User;
userEmail: 'sender@example.com',
name: { firstName: 'John', lastName: 'Doe' },
} as WorkspaceMemberWorkspaceEntity;
const workspace = {
displayName: 'Test Workspace',
logo: '/logo.png',

View File

@ -6,14 +6,17 @@ import { User } from 'src/engine/core-modules/user/user.entity';
import { RunWorkflowVersionInput } from 'src/engine/core-modules/workflow/dtos/run-workflow-version-input.dto';
import { WorkflowRunDTO } from 'src/engine/core-modules/workflow/dtos/workflow-run.dto';
import { WorkflowTriggerGraphqlApiExceptionFilter } from 'src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspaceMemberId } from 'src/engine/decorators/auth/auth-workspace-member-id.decorator';
import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
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 { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
@Resolver()
@UseGuards(
@ -27,6 +30,7 @@ import { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-t
)
export class WorkflowTriggerResolver {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly workflowTriggerWorkspaceService: WorkflowTriggerWorkspaceService,
) {}
@ -50,19 +54,31 @@ export class WorkflowTriggerResolver {
@Mutation(() => WorkflowRunDTO)
async runWorkflowVersion(
@AuthWorkspaceMemberId() workspaceMemberId: string,
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
@Args('input') { workflowVersionId, payload }: RunWorkflowVersionInput,
) {
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
);
const workspaceMember = await workspaceMemberRepository.findOneOrFail({
where: {
userId: user.id,
},
});
return await this.workflowTriggerWorkspaceService.runWorkflowVersion({
workflowVersionId,
payload: payload ?? {},
createdBy: buildCreatedByFromFullNameMetadata({
fullNameMetadata: {
firstName: user.firstName,
lastName: user.lastName,
firstName: workspaceMember.name.firstName,
lastName: workspaceMember.name.lastName,
},
workspaceMemberId: workspaceMemberId,
workspaceMemberId: workspaceMember.id,
}),
});
}

View File

@ -12,10 +12,10 @@ import { EmailService } from 'src/engine/core-modules/email/email.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { User } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceInvitationException } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.exception';
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { WorkspaceInvitationService } from './workspace-invitation.service';
@ -148,7 +148,10 @@ describe('WorkspaceInvitationService', () => {
inviteHash: 'invite-hash',
displayName: 'Test Workspace',
} as Workspace;
const sender = { email: 'sender@example.com', firstName: 'Sender' };
const sender = {
userEmail: 'sender@example.com',
name: { firstName: 'Sender' },
};
jest.spyOn(service, 'createWorkspaceInvitation').mockResolvedValue({
context: { email: 'test@example.com' },
@ -166,7 +169,7 @@ describe('WorkspaceInvitationService', () => {
const result = await service.sendInvitations(
emails,
workspace,
sender as User,
sender as WorkspaceMemberWorkspaceEntity,
);
expect(result.success).toBe(true);

View File

@ -25,7 +25,6 @@ import { EmailService } from 'src/engine/core-modules/email/email.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { User } from 'src/engine/core-modules/user/user.entity';
import { SendInvitationsOutput } from 'src/engine/core-modules/workspace-invitation/dtos/send-invitations.output';
import { castAppTokenToWorkspaceInvitationUtil } from 'src/engine/core-modules/workspace-invitation/utils/cast-app-token-to-workspace-invitation.util';
import {
@ -33,6 +32,7 @@ import {
WorkspaceInvitationExceptionCode,
} from 'src/engine/core-modules/workspace-invitation/workspace-invitation.exception';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Injectable()
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
@ -228,7 +228,7 @@ export class WorkspaceInvitationService {
async resendWorkspaceInvitation(
appTokenId: string,
workspace: Workspace,
sender: User,
sender: WorkspaceMemberWorkspaceEntity,
) {
const appToken = await this.appTokenRepository.findOne({
where: {
@ -253,7 +253,7 @@ export class WorkspaceInvitationService {
async sendInvitations(
emails: string[],
workspace: Workspace,
sender: User,
sender: WorkspaceMemberWorkspaceEntity,
usePersonalInvitation = true,
): Promise<SendInvitationsOutput> {
if (!workspace?.inviteHash) {
@ -306,14 +306,13 @@ export class WorkspaceInvitationService {
: {},
});
// Todo: sender name and locale should come from workspace member not user!
const emailData = {
link: link.toString(),
workspace: { name: workspace.displayName, logo: workspace.logo },
sender: {
email: sender.email,
firstName: sender.firstName,
lastName: sender.lastName,
email: sender.userEmail,
firstName: sender.name.firstName,
lastName: sender.name.lastName,
},
serverUrl: this.twentyConfigService.get('SERVER_URL'),
locale: sender.locale as keyof typeof APP_LOCALES,
@ -328,7 +327,7 @@ export class WorkspaceInvitationService {
i18n.activate(sender.locale);
await this.emailService.send({
from: `${sender.firstName} ${sender.lastName} (via Twenty) <${this.twentyConfigService.get('EMAIL_FROM_ADDRESS')}>`,
from: `${sender.name.firstName} ${sender.name.lastName} (via Twenty) <${this.twentyConfigService.get('EMAIL_FROM_ADDRESS')}>`,
to: invitation.value.email,
subject: t`Join your team on Twenty`,
text,

View File

@ -14,6 +14,8 @@ 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 { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { SendInvitationsInput } from './dtos/send-invitations.input';
@ -25,6 +27,7 @@ import { SendInvitationsInput } from './dtos/send-invitations.input';
@Resolver()
export class WorkspaceInvitationResolver {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly workspaceInvitationService: WorkspaceInvitationService,
private readonly fileService: FileService,
) {}
@ -47,10 +50,22 @@ export class WorkspaceInvitationResolver {
@AuthWorkspace() workspace: Workspace,
@AuthUser() user: User,
) {
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
);
const workspaceMember = await workspaceMemberRepository.findOneOrFail({
where: {
userId: user.id,
},
});
return this.workspaceInvitationService.resendWorkspaceInvitation(
appTokenId,
workspace,
user,
workspaceMember,
);
}
@ -75,10 +90,22 @@ export class WorkspaceInvitationResolver {
});
}
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
);
const workspaceMember = await workspaceMemberRepository.findOneOrFail({
where: {
userId: user.id,
},
});
return await this.workspaceInvitationService.sendInvitations(
sendInviteLinkInput.emails,
{ ...workspace, logo: workspaceLogoWithToken },
user,
workspaceMember,
);
}
}