Fix workspace logo, cookie security, and improve workspace impersonation (#14838)

3 small fixes:
- Fix workspace logo
- Improve cookie security
- Improve workspace impersonation
This commit is contained in:
Félix Malfait
2025-10-02 18:36:52 +02:00
committed by GitHub
parent 388e49a8cf
commit d4914aa130
18 changed files with 128 additions and 14 deletions
@@ -7,6 +7,7 @@ import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-pan
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
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 { FileService } from 'src/engine/core-modules/file/services/file.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { User } from 'src/engine/core-modules/user/user.entity';
@@ -82,6 +83,10 @@ describe('AdminPanelService', () => {
}),
},
},
{
provide: FileService,
useValue: {},
},
],
}).compile();
@@ -9,6 +9,7 @@ import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { HealthModule } from 'src/engine/core-modules/health/health.module';
import { ImpersonationModule } from 'src/engine/core-modules/impersonation/impersonation.module';
import { RedisClientModule } from 'src/engine/core-modules/redis-client/redis-client.module';
@@ -21,6 +22,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
TypeOrmModule.forFeature([User]),
AuthModule,
DomainManagerModule,
FileModule,
HealthModule,
RedisClientModule,
TerminusModule,
@@ -20,6 +20,7 @@ import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/l
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { type FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { type ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables';
import { CONFIG_VARIABLES_GROUP_METADATA } from 'src/engine/core-modules/twenty-config/constants/config-variables-group-metadata';
import { type ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
@@ -34,15 +35,21 @@ export class AdminPanelService {
private readonly twentyConfigService: TwentyConfigService,
private readonly domainManagerService: DomainManagerService,
private readonly auditService: AuditService,
private readonly fileService: FileService,
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
async userLookup(userIdentifier: string): Promise<UserLookup> {
const isEmail = userIdentifier.includes('@');
const normalizedIdentifier = isEmail
? userIdentifier.toLowerCase()
: userIdentifier;
const targetUser = await this.userRepository.findOne({
where: isEmail ? { email: userIdentifier } : { id: userIdentifier },
where: isEmail
? { email: normalizedIdentifier }
: { id: normalizedIdentifier },
relations: {
userWorkspaces: {
workspace: {
@@ -57,7 +64,9 @@ export class AdminPanelService {
userValidator.assertIsDefinedOrThrow(
targetUser,
new AuthException('User not found', AuthExceptionCode.INVALID_INPUT),
new AuthException('User not found', AuthExceptionCode.INVALID_INPUT, {
userFriendlyMessage: 'User not found. Please check the email or ID.',
}),
);
const allFeatureFlagKeys = Object.values(FeatureFlagKey);
@@ -73,8 +82,18 @@ export class AdminPanelService {
id: userWorkspace.workspace.id,
name: userWorkspace.workspace.displayName ?? '',
totalUsers: userWorkspace.workspace.workspaceUsers.length,
logo: userWorkspace.workspace.logo,
logo: userWorkspace.workspace.logo
? this.fileService.signFileUrl({
url: userWorkspace.workspace.logo,
workspaceId: userWorkspace.workspace.id,
})
: userWorkspace.workspace.logo,
allowImpersonation: userWorkspace.workspace.allowImpersonation,
workspaceUrls: this.domainManagerService.getWorkspaceUrls({
subdomain: userWorkspace.workspace.subdomain,
customDomain: userWorkspace.workspace.customDomain,
isCustomDomainEnabled: userWorkspace.workspace.isCustomDomainEnabled,
}),
users: userWorkspace.workspace.workspaceUsers.map((workspaceUser) => ({
id: workspaceUser.user.id,
email: workspaceUser.user.email,
@@ -2,6 +2,7 @@ import { Field, ObjectType } from '@nestjs/graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { WorkspaceUrls } from 'src/engine/core-modules/workspace/dtos/workspace-urls.dto';
@ObjectType()
class UserInfo {
@@ -35,6 +36,9 @@ class WorkspaceInfo {
@Field(() => Number)
totalUsers: number;
@Field(() => WorkspaceUrls)
workspaceUrls: WorkspaceUrls;
@Field(() => [UserInfo])
users: UserInfo[];
@@ -2,14 +2,16 @@ import { Module } from '@nestjs/common';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { ApprovedAccessDomain } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.entity';
import { ApprovedAccessDomainResolver } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.resolver';
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 { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
@Module({
imports: [
DomainManagerModule,
FileModule,
NestjsQueryTypeOrmModule.forFeature([ApprovedAccessDomain]),
],
exports: [ApprovedAccessDomainService],
@@ -18,6 +18,7 @@ import {
import { approvedAccessDomainValidator } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.validate';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@@ -31,6 +32,7 @@ export class ApprovedAccessDomainService {
private readonly emailService: EmailService,
private readonly twentyConfigService: TwentyConfigService,
private readonly domainManagerService: DomainManagerService,
private readonly fileService: FileService,
) {}
async sendApprovedAccessDomainValidationEmail(
@@ -70,7 +72,15 @@ export class ApprovedAccessDomainService {
const emailTemplate = SendApprovedAccessDomainValidation({
link: link.toString(),
workspace: { name: workspace.displayName, logo: workspace.logo },
workspace: {
name: workspace.displayName,
logo: workspace.logo
? this.fileService.signFileUrl({
url: workspace.logo,
workspaceId: workspace.id,
})
: workspace.logo,
},
domain: approvedAccessDomain.domain,
sender: {
email: sender.userEmail,
@@ -12,6 +12,7 @@ import {
} from 'src/engine/core-modules/approved-access-domain/approved-access-domain.exception';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@@ -67,6 +68,14 @@ describe('ApprovedAccessDomainService', () => {
buildWorkspaceURL: jest.fn(),
},
},
{
provide: FileService,
useValue: {
signFileUrl: jest
.fn()
.mockReturnValue('https://signed-url.com/logo.png'),
},
},
],
}).compile();
@@ -32,6 +32,8 @@ export const getSessionStorageOptions = (
proxy: true,
cookie: {
secure: !!(SERVER_URL && SERVER_URL.startsWith('https')),
httpOnly: true,
sameSite: 'lax',
maxAge: 1000 * 60 * 30, // 30 minutes
},
};
@@ -381,7 +381,12 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
id: workspace.id,
displayName: workspace.displayName,
workspaceUrls: this.domainManagerService.getWorkspaceUrls(workspace),
logo: workspace.logo,
logo: workspace.logo
? this.fileService.signFileUrl({
url: workspace.logo,
workspaceId: workspace.id,
})
: workspace.logo,
sso:
workspace.workspaceSSOIdentityProviders?.reduce(
(acc, identityProvider) =>
@@ -9,6 +9,8 @@ import {
} from 'src/engine/core-modules/app-token/app-token.entity';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.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';
@@ -16,7 +18,6 @@ import { WorkspaceInvitationException } from 'src/engine/core-modules/workspace-
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import { WorkspaceInvitationService } from './workspace-invitation.service';
@@ -103,6 +104,14 @@ describe('WorkspaceInvitationService', () => {
}),
},
},
{
provide: FileService,
useValue: {
signFileUrl: jest
.fn()
.mockReturnValue('https://signed-url.com/logo.png'),
},
},
],
}).compile();
@@ -22,6 +22,7 @@ import {
} from 'src/engine/core-modules/auth/auth.exception';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@@ -47,6 +48,7 @@ export class WorkspaceInvitationService {
private readonly onboardingService: OnboardingService,
private readonly domainManagerService: DomainManagerService,
private readonly i18nService: I18nService,
private readonly fileService: FileService,
) {}
async validatePersonalInvitation({
@@ -294,7 +296,15 @@ export class WorkspaceInvitationService {
const emailData = {
link: link.toString(),
workspace: { name: workspace.displayName, logo: workspace.logo },
workspace: {
name: workspace.displayName,
logo: workspace.logo
? this.fileService.signFileUrl({
url: workspace.logo,
workspaceId: workspace.id,
})
: workspace.logo,
},
sender: {
email: sender.userEmail,
firstName: sender.name.firstName,