Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix broken image urls in Settings > Profile and Invite To Workspace Email #8942

Merged
merged 5 commits into from
Dec 7, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/twenty-emails/src/emails/send-invite-link.email.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type SendInviteLinkEmailProps = {
firstName: string;
lastName: string;
};
serverUrl?: string;
serverUrl: string;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, I think we should stop authorizing undefined in our typings unless this is needed. The caller should handle the case. This will reduce code complexity

};

export const SendInviteLinkEmail = ({
Expand All @@ -29,7 +29,9 @@ export const SendInviteLinkEmail = ({
sender,
serverUrl,
}: SendInviteLinkEmailProps) => {
const workspaceLogo = getImageAbsoluteURI(workspace.logo, serverUrl);
const workspaceLogo = workspace.logo
? getImageAbsoluteURI(workspace.logo, serverUrl)
: null;

return (
<BaseEmail width={333}>
Expand Down
13 changes: 3 additions & 10 deletions packages/twenty-emails/src/utils/getImageAbsoluteURI.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
export const getImageAbsoluteURI = (
imageUrl?: string | null,
serverUrl?: string,
) => {
if (!imageUrl) {
return null;
}

if (imageUrl?.startsWith('https:')) {
export const getImageAbsoluteURI = (imageUrl: string, serverUrl: string) => {
if (imageUrl.startsWith('https:') || imageUrl.startsWith('http:')) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Removing optional parameters could break existing code that passes undefined/null. Consider keeping parameters optional with default values.

return imageUrl;
}

return serverUrl?.endsWith('/')
return serverUrl.endsWith('/')
? `${serverUrl.substring(0, serverUrl.length - 1)}/files/${imageUrl}`
: `${serverUrl || ''}/files/${imageUrl}`;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Using serverUrl || '' is redundant since serverUrl is now required. This fallback should be removed.

7 changes: 4 additions & 3 deletions packages/twenty-front/src/modules/auth/components/Logo.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import styled from '@emotion/styled';

import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI';
import { getImageAbsoluteURI, isDefined } from 'twenty-ui';

type LogoProps = {
primaryLogo?: string | null;
Expand Down Expand Up @@ -49,7 +48,9 @@ export const Logo = (props: LogoProps) => {
const primaryLogoUrl = getImageAbsoluteURI(
props.primaryLogo ?? defaultPrimaryLogoUrl,
);
const secondaryLogoUrl = getImageAbsoluteURI(props.secondaryLogo);
const secondaryLogoUrl = isDefined(props.secondaryLogo)
? getImageAbsoluteURI(props.secondaryLogo)
: null;

return (
<StyledContainer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@ import {
NavigationDrawerProps,
} from '@/ui/navigation/navigation-drawer/components/NavigationDrawer';
import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState';
import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI';

import { useIsSettingsDrawer } from '@/navigation/hooks/useIsSettingsDrawer';

import { AdvancedSettingsToggle } from 'twenty-ui';
import { AdvancedSettingsToggle, getImageAbsoluteURI } from 'twenty-ui';
import { MainNavigationDrawerItems } from './MainNavigationDrawerItems';

export type AppNavigationDrawerProps = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getLogoUrlFromDomainName } from '~/utils';
import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI';
import { isDefined } from '~/utils/isDefined';

import { Company } from '@/companies/types/Company';
import { getCompanyDomainName } from '@/object-metadata/utils/getCompanyDomainName';
import { getImageAbsoluteURI } from 'twenty-ui';
import { getImageIdentifierFieldValue } from './getImageIdentifierFieldValue';

export const getAvatarUrl = (
Expand All @@ -25,7 +25,9 @@ export const getAvatarUrl = (
}

if (objectNameSingular === CoreObjectNameSingular.Person) {
return getImageAbsoluteURI(record.avatarUrl) ?? '';
return isDefined(record.avatarUrl)
? getImageAbsoluteURI(record.avatarUrl)
: '';
}

const imageIdentifierFieldValue = getImageIdentifierFieldValue(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import React, { useMemo } from 'react';
import { Button, IconPhotoUp, IconTrash, IconUpload, IconX } from 'twenty-ui';
import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI';
import React from 'react';
import {
Button,
IconPhotoUp,
IconTrash,
IconUpload,
IconX,
getImageAbsoluteURI,
} from 'twenty-ui';
import { isDefined } from '~/utils/isDefined';

const StyledContainer = styled.div`
Expand Down Expand Up @@ -109,7 +115,7 @@ export const ImageInput = ({
hiddenFileInput.current?.click();
};

const pictureURI = useMemo(() => getImageAbsoluteURI(picture), [picture]);
const pictureURI = isDefined(picture) ? getImageAbsoluteURI(picture) : null;

return (
<StyledContainer className={className}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { Workspaces } from '@/auth/states/workspaces';
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
Expand All @@ -17,9 +18,8 @@ import {
IconChevronDown,
MenuItemSelectAvatar,
UndecoratedLink,
getImageAbsoluteURI,
} from 'twenty-ui';
import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI';
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';

const StyledLogo = styled.div<{ logo: string }>`
background: url(${({ logo }) => logo});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Helmet } from 'react-helmet-async';
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
import { Helmet } from 'react-helmet-async';
import { useRecoilValue } from 'recoil';
import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI';
import { getImageAbsoluteURI } from 'twenty-ui';

export const PageFavicon = () => {
const workspacePublicData = useRecoilValue(workspacePublicDataState);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export const SettingsAdminFeatureFlags = () => {
title: workspace.name,
logo:
getImageAbsoluteURI(
workspace.logo === null ? DEFAULT_WORKSPACE_LOGO : workspace.logo,
isDefined(workspace.logo) ? workspace.logo : DEFAULT_WORKSPACE_LOGO,
) ?? '',
Comment on lines 109 to 111
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: This logic could still result in undefined being passed to getImageAbsoluteURI if workspace.logo is null. Consider using workspace.logo ?? DEFAULT_WORKSPACE_LOGO instead.

})) ?? [];

Expand Down
26 changes: 0 additions & 26 deletions packages/twenty-front/src/utils/image/getImageAbsoluteURI.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Removing this file removes null/undefined handling and proper URL path construction. The twenty-ui version may need these features added back to maintain full functionality.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: The removed version handled leading slashes in image URLs differently than the twenty-ui version. This could cause broken image URLs if image paths start with '/'.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: The URL construction using URL class was more robust than string concatenation. Consider keeping this approach for better URL handling.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export class FilePathGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const query = request.query;
const urlParamsV2 = new URLSearchParams(request.originalUrl);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: urlParamsV2 is declared but never used


if (!query || !query['token']) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { Module } from '@nestjs/common';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';

import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
import { WorkspaceInvitationResolver } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.resolver';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';

@Module({
imports: [
Expand All @@ -17,6 +18,7 @@ import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/doma
[AppToken, UserWorkspace, Workspace],
'core',
),
FileModule,
OnboardingModule,
],
exports: [WorkspaceInvitationService],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';

import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
import { WorkspaceInvitation } from 'src/engine/core-modules/workspace-invitation/dtos/workspace-invitation.dto';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { SendInvitationsOutput } from 'src/engine/core-modules/workspace-invitation/dtos/send-invitations.output';
import { WorkspaceInvitation } from 'src/engine/core-modules/workspace-invitation/dtos/workspace-invitation.dto';
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
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 { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';

import { SendInvitationsInput } from './dtos/send-invitations.input';

Expand All @@ -18,6 +19,7 @@ import { SendInvitationsInput } from './dtos/send-invitations.input';
export class WorkspaceInvitationResolver {
constructor(
private readonly workspaceInvitationService: WorkspaceInvitationService,
private readonly fileService: FileService,
) {}

@Mutation(() => String)
Expand Down Expand Up @@ -57,9 +59,19 @@ export class WorkspaceInvitationResolver {
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
): Promise<SendInvitationsOutput> {
let workspaceLogoWithToken = '';

if (workspace.logo) {
const workspaceLogoToken = await this.fileService.encodeFileToken({
workspaceId: workspace.id,
});

workspaceLogoWithToken = `${workspace.logo}?token=${workspaceLogoToken}`;
}
Comment on lines +64 to +70
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: token generation should be wrapped in try/catch to handle potential encoding errors gracefully


return await this.workspaceInvitationService.sendInvitations(
sendInviteLinkInput.emails,
workspace,
{ ...workspace, logo: workspaceLogoWithToken },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: spreading workspace and overriding logo could cause issues if workspace is frozen/sealed object

user,
);
}
Expand Down
9 changes: 7 additions & 2 deletions packages/twenty-ui/src/display/avatar/components/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import { AvatarSize } from '@ui/display/avatar/types/AvatarSize';
import { AvatarType } from '@ui/display/avatar/types/AvatarType';
import { IconComponent } from '@ui/display/icon/types/IconComponent';
import { ThemeContext } from '@ui/theme';
import { Nullable, getImageAbsoluteURI, stringToHslColor } from '@ui/utilities';
import {
Nullable,
getImageAbsoluteURI,
isDefined,
stringToHslColor,
} from '@ui/utilities';

const StyledAvatar = styled.div<{
size: AvatarSize;
Expand Down Expand Up @@ -82,7 +87,7 @@ export const Avatar = ({
);

const avatarImageURI = useMemo(
() => getImageAbsoluteURI(avatarUrl),
() => (isDefined(avatarUrl) ? getImageAbsoluteURI(avatarUrl) : null),
[avatarUrl],
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { getImageAbsoluteURI } from '../getImageAbsoluteURI';

describe('getImageAbsoluteURI', () => {
it('should return null if imageUrl is null', () => {
const imageUrl = null;
it('should return absolute url if the imageUrl is an absolute url', () => {
const imageUrl = 'https://XXX';
const result = getImageAbsoluteURI(imageUrl);
expect(result).toBeNull();
expect(result).toBe(imageUrl);
});

it('should return absolute url if the imageUrl is an absolute url', () => {
const imageUrl = 'https://XXX';
it('should return absolute url if the imageUrl is an absolute unsecure url', () => {
const imageUrl = 'http://XXX';
const result = getImageAbsoluteURI(imageUrl);
expect(result).toBe(imageUrl);
});
Comment on lines +4 to 14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: test cases for https and http are redundant since they test the same logic branch in the implementation

Expand Down
12 changes: 2 additions & 10 deletions packages/twenty-ui/src/utilities/image/getImageAbsoluteURI.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
import { REACT_APP_SERVER_BASE_URL } from '@ui/utilities/config';

// TODO: this is a code smell trying to guess whether it's a relative path or not
// We should instead put the meaning onto our variables and parameters
// imageUrl should be either imageAbsoluteURL or imageRelativeServerPath
// But we need to refactor the chain of calls to this function
export const getImageAbsoluteURI = (imageUrl?: string | null) => {
if (!imageUrl) {
return null;
}

if (imageUrl?.startsWith('http')) {
export const getImageAbsoluteURI = (imageUrl: string) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Removing null/undefined handling could break existing code that passes undefined. Consider keeping parameter optional or adding runtime validation.

if (imageUrl.startsWith('https:') || imageUrl.startsWith('http:')) {
return imageUrl;
}

Expand Down
Loading