11744 emails broken image in emails (#12265)

- refactor file tokens
- update file token management
  - generate one token per file per workspaceId
  - move token from query params to url path
This commit is contained in:
martmull
2025-05-26 22:05:21 +02:00
committed by GitHub
parent 69badf2a66
commit aa58259019
53 changed files with 775 additions and 386 deletions
@@ -1,8 +1,11 @@
import { Injectable } from '@nestjs/common';
import { buildSignedPath } from 'twenty-shared/utils';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { DeletedWorkspaceMember } from 'src/engine/core-modules/user/dtos/deleted-workspace-member.dto';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { extractFilenameFromPath } from 'src/engine/core-modules/file/utils/extract-file-id-from-path.utils';
@Injectable()
export class DeletedWorkspaceMemberTranspiler {
@@ -15,12 +18,15 @@ export class DeletedWorkspaceMemberTranspiler {
workspaceMember: Pick<WorkspaceMemberWorkspaceEntity, 'avatarUrl' | 'id'>;
workspaceId: string;
}): string {
const avatarUrlToken = this.fileService.encodeFileToken({
workspaceMemberId: workspaceMember.id,
workspaceId: workspaceId,
const signedPayload = this.fileService.encodeFileToken({
filename: extractFilenameFromPath(workspaceMember.avatarUrl),
workspaceId,
});
return `${workspaceMember.avatarUrl}?token=${avatarUrlToken}`;
return buildSignedPath({
path: workspaceMember.avatarUrl,
token: signedPayload,
});
}
toDeletedWorkspaceMemberDto(
@@ -14,6 +14,7 @@ import crypto from 'crypto';
import { GraphQLJSONObject } from 'graphql-type-json';
import { FileUpload, GraphQLUpload } from 'graphql-upload';
import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants';
import { buildSignedPath } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { In, Repository } from 'typeorm';
@@ -52,6 +53,8 @@ import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { AccountsToReconnectKeys } from 'src/modules/connected-account/types/accounts-to-reconnect-key-value.type';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
import { SignedFileDTO } from 'src/engine/core-modules/file/file-upload/dtos/signed-file.dto';
import { extractFilenameFromPath } from 'src/engine/core-modules/file/utils/extract-file-id-from-path.utils';
const getHMACKey = (email?: string, key?: string | null) => {
if (!email || !key) return null;
@@ -186,11 +189,14 @@ export class UserResolver {
if (workspaceMember && workspaceMember.avatarUrl) {
const avatarUrlToken = this.fileService.encodeFileToken({
workspaceMemberId: workspaceMember.id,
filename: extractFilenameFromPath(workspaceMember.avatarUrl),
workspaceId: workspace.id,
});
workspaceMember.avatarUrl = `${workspaceMember.avatarUrl}?token=${avatarUrlToken}`;
workspaceMember.avatarUrl = buildSignedPath({
path: workspaceMember.avatarUrl,
token: avatarUrlToken,
});
}
// TODO Refactor to be transpiled to WorkspaceMember instead
@@ -235,11 +241,14 @@ export class UserResolver {
for (const workspaceMemberEntity of workspaceMemberEntities) {
if (workspaceMemberEntity.avatarUrl) {
const avatarUrlToken = this.fileService.encodeFileToken({
workspaceMemberId: workspaceMemberEntity.id,
filename: extractFilenameFromPath(workspaceMemberEntity.avatarUrl),
workspaceId: workspace.id,
});
workspaceMemberEntity.avatarUrl = `${workspaceMemberEntity.avatarUrl}?token=${avatarUrlToken}`;
workspaceMemberEntity.avatarUrl = buildSignedPath({
path: workspaceMemberEntity.avatarUrl,
token: avatarUrlToken,
});
}
// TODO Refactor to be transpiled to WorkspaceMember instead
@@ -314,13 +323,13 @@ export class UserResolver {
return getHMACKey(parent.email, key);
}
@Mutation(() => String)
@Mutation(() => SignedFileDTO)
async uploadProfilePicture(
@AuthUser() { id }: User,
@AuthWorkspace() { id: workspaceId }: Workspace,
@Args({ name: 'file', type: () => GraphQLUpload })
{ createReadStream, filename, mimetype }: FileUpload,
): Promise<string> {
): Promise<SignedFileDTO> {
if (!id) {
throw new Error('User not found');
}
@@ -329,7 +338,7 @@ export class UserResolver {
const buffer = await streamToBuffer(stream);
const fileFolder = FileFolder.ProfilePicture;
const { paths } = await this.fileUploadService.uploadImage({
const { files } = await this.fileUploadService.uploadImage({
file: buffer,
filename,
mimeType: mimetype,
@@ -337,11 +346,11 @@ export class UserResolver {
workspaceId,
});
const fileToken = this.fileService.encodeFileToken({
workspaceId: workspaceId,
});
if (!files.length) {
throw new Error('Failed to upload profile picture');
}
return `${paths[0]}?token=${fileToken}`;
return files[0];
}
@Mutation(() => User)