Various fixes: profilePicture / logo upload, imageIdentifiers (#6530)

In this PR:
- refactoring auth module to extract a jwt module that can be re-used
from other part of the app (avoiding circular dependencies file module
=> auth => file (file and auth both need jwt actually)
- activating imageIdentfier on person on workspace creation (this will
put back the images on people)
- fixing picture upload (we were missing some fileToken)
This commit is contained in:
Charles Bochet
2024-08-04 15:08:25 +02:00
committed by GitHub
parent e787215e15
commit c543716381
15 changed files with 143 additions and 91 deletions

View File

@ -51,7 +51,7 @@ export const ProfilePictureUploader = () => {
setUploadController(null); setUploadController(null);
setErrorMessage(null); setErrorMessage(null);
const avatarUrl = result?.data?.uploadProfilePicture; const avatarUrl = result?.data?.uploadProfilePicture.split('?')[0];
if (!avatarUrl) { if (!avatarUrl) {
throw new Error('Avatar URL not found'); throw new Error('Avatar URL not found');
@ -64,7 +64,10 @@ export const ProfilePictureUploader = () => {
}, },
}); });
setCurrentWorkspaceMember({ ...currentWorkspaceMember, avatarUrl }); setCurrentWorkspaceMember({
...currentWorkspaceMember,
avatarUrl: result?.data?.uploadProfilePicture,
});
return result; return result;
} catch (error) { } catch (error) {

View File

@ -7,6 +7,7 @@ import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/consta
import { DEFAULT_WORKSPACE_NAME } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceName'; import { DEFAULT_WORKSPACE_NAME } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceName';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { isNonEmptyString } from '@sniptt/guards';
import { NavigationDrawerCollapseButton } from './NavigationDrawerCollapseButton'; import { NavigationDrawerCollapseButton } from './NavigationDrawerCollapseButton';
const StyledContainer = styled.div<{ isMultiWorkspace: boolean }>` const StyledContainer = styled.div<{ isMultiWorkspace: boolean }>`
@ -65,7 +66,9 @@ export const NavigationDrawerHeader = ({
<MultiWorkspaceDropdownButton workspaces={workspaces} /> <MultiWorkspaceDropdownButton workspaces={workspaces} />
) : ( ) : (
<> <>
<StyledLogo logo={logo} /> <StyledLogo
logo={isNonEmptyString(logo) ? logo : DEFAULT_WORKSPACE_LOGO}
/>
<StyledName>{name}</StyledName> <StyledName>{name}</StyledName>
</> </>
)} )}

View File

@ -1,7 +1,6 @@
/* eslint-disable no-restricted-imports */ /* eslint-disable no-restricted-imports */
import { HttpModule } from '@nestjs/axios'; import { HttpModule } from '@nestjs/axios';
import { forwardRef, Module } from '@nestjs/common'; import { forwardRef, Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
@ -16,13 +15,13 @@ import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-u
import { TokenService } from 'src/engine/core-modules/auth/services/token.service'; import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module'; import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module'; import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module'; import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { User } from 'src/engine/core-modules/user/user.entity'; import { User } from 'src/engine/core-modules/user/user.entity';
import { UserModule } from 'src/engine/core-modules/user/user.module'; import { UserModule } from 'src/engine/core-modules/user/user.module';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module'; import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
@ -32,27 +31,15 @@ import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/stan
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module'; import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module';
import { AuthResolver } from './auth.resolver'; import { AuthResolver } from './auth.resolver';
import { AuthService } from './services/auth.service'; import { AuthService } from './services/auth.service';
import { JwtAuthStrategy } from './strategies/jwt.auth.strategy'; import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
const jwtModule = JwtModule.registerAsync({
useFactory: async (environmentService: EnvironmentService) => {
return {
secret: environmentService.get('ACCESS_TOKEN_SECRET'),
signOptions: {
expiresIn: environmentService.get('ACCESS_TOKEN_EXPIRES_IN'),
},
};
},
inject: [EnvironmentService],
});
@Module({ @Module({
imports: [ imports: [
jwtModule, JwtModule,
FileUploadModule, FileUploadModule,
DataSourceModule, DataSourceModule,
forwardRef(() => UserModule), forwardRef(() => UserModule),
@ -89,6 +76,6 @@ const jwtModule = JwtModule.registerAsync({
GoogleAPIsService, GoogleAPIsService,
AppTokenService, AppTokenService,
], ],
exports: [jwtModule, TokenService], exports: [TokenService],
}) })
export class AuthModule {} export class AuthModule {}

View File

@ -1,25 +1,25 @@
import { Test, TestingModule } from '@nestjs/testing';
import { JwtService } from '@nestjs/jwt';
import { getRepositoryToken } from '@nestjs/typeorm';
import { import {
BadRequestException, BadRequestException,
InternalServerErrorException, InternalServerErrorException,
NotFoundException, NotFoundException,
} from '@nestjs/common'; } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import crypto from 'crypto'; import crypto from 'crypto';
import { IsNull, MoreThan, Repository } from 'typeorm'; import { IsNull, MoreThan, Repository } from 'typeorm';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { import {
AppToken, AppToken,
AppTokenType, AppTokenType,
} from 'src/engine/core-modules/app-token/app-token.entity'; } from 'src/engine/core-modules/app-token/app-token.entity';
import { User } from 'src/engine/core-modules/user/user.entity';
import { JwtAuthStrategy } from 'src/engine/core-modules/auth/strategies/jwt.auth.strategy'; import { JwtAuthStrategy } from 'src/engine/core-modules/auth/strategies/jwt.auth.strategy';
import { EmailService } from 'src/engine/integrations/email/email.service'; import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { EmailService } from 'src/engine/integrations/email/email.service';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { TokenService } from './token.service'; import { TokenService } from './token.service';

View File

@ -7,50 +7,50 @@ import {
UnauthorizedException, UnauthorizedException,
UnprocessableEntityException, UnprocessableEntityException,
} from '@nestjs/common'; } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import crypto from 'crypto'; import crypto from 'crypto';
import { addMilliseconds, differenceInMilliseconds } from 'date-fns';
import ms from 'ms';
import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken';
import { IsNull, MoreThan, Repository } from 'typeorm';
import { Request } from 'express';
import { ExtractJwt } from 'passport-jwt';
import { render } from '@react-email/render'; import { render } from '@react-email/render';
import { addMilliseconds, differenceInMilliseconds } from 'date-fns';
import { Request } from 'express';
import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken';
import ms from 'ms';
import { ExtractJwt } from 'passport-jwt';
import { PasswordResetLinkEmail } from 'twenty-emails'; import { PasswordResetLinkEmail } from 'twenty-emails';
import { IsNull, MoreThan, Repository } from 'typeorm';
import { import {
JwtAuthStrategy, AppToken,
JwtPayload, AppTokenType,
} from 'src/engine/core-modules/auth/strategies/jwt.auth.strategy'; } from 'src/engine/core-modules/app-token/app-token.entity';
import { assert } from 'src/utils/assert'; import { EmailPasswordResetLink } from 'src/engine/core-modules/auth/dto/email-password-reset-link.entity';
import { ExchangeAuthCode } from 'src/engine/core-modules/auth/dto/exchange-auth-code.entity';
import { ExchangeAuthCodeInput } from 'src/engine/core-modules/auth/dto/exchange-auth-code.input';
import { InvalidatePassword } from 'src/engine/core-modules/auth/dto/invalidate-password.entity';
import { import {
ApiKeyToken, ApiKeyToken,
AuthToken, AuthToken,
AuthTokens, AuthTokens,
PasswordResetToken, PasswordResetToken,
} from 'src/engine/core-modules/auth/dto/token.entity'; } from 'src/engine/core-modules/auth/dto/token.entity';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import {
AppToken,
AppTokenType,
} from 'src/engine/core-modules/app-token/app-token.entity';
import { ValidatePasswordResetToken } from 'src/engine/core-modules/auth/dto/validate-password-reset-token.entity'; import { ValidatePasswordResetToken } from 'src/engine/core-modules/auth/dto/validate-password-reset-token.entity';
import { EmailService } from 'src/engine/integrations/email/email.service'; import {
import { InvalidatePassword } from 'src/engine/core-modules/auth/dto/invalidate-password.entity'; JwtAuthStrategy,
import { EmailPasswordResetLink } from 'src/engine/core-modules/auth/dto/email-password-reset-link.entity'; JwtPayload,
} from 'src/engine/core-modules/auth/strategies/jwt.auth.strategy';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { ExchangeAuthCodeInput } from 'src/engine/core-modules/auth/dto/exchange-auth-code.input'; import { EmailService } from 'src/engine/integrations/email/email.service';
import { ExchangeAuthCode } from 'src/engine/core-modules/auth/dto/exchange-auth-code.entity'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { assert } from 'src/utils/assert';
@Injectable() @Injectable()
export class TokenService { export class TokenService {
constructor( constructor(
private readonly jwtService: JwtService, private readonly jwtWrapperService: JwtWrapperService,
private readonly jwtStrategy: JwtAuthStrategy, private readonly jwtStrategy: JwtAuthStrategy,
private readonly environmentService: EnvironmentService, private readonly environmentService: EnvironmentService,
@InjectRepository(User, 'core') @InjectRepository(User, 'core')
@ -90,7 +90,7 @@ export class TokenService {
}; };
return { return {
token: this.jwtService.sign(jwtPayload), token: this.jwtWrapperService.sign(jwtPayload),
expiresAt, expiresAt,
}; };
} }
@ -116,7 +116,7 @@ export class TokenService {
await this.appTokenRepository.save(refreshToken); await this.appTokenRepository.save(refreshToken);
return { return {
token: this.jwtService.sign(jwtPayload, { token: this.jwtWrapperService.sign(jwtPayload, {
secret, secret,
expiresIn, expiresIn,
// Jwtid will be used to link RefreshToken entity to this token // Jwtid will be used to link RefreshToken entity to this token
@ -137,7 +137,7 @@ export class TokenService {
}; };
return { return {
token: this.jwtService.sign(jwtPayload, { token: this.jwtWrapperService.sign(jwtPayload, {
secret, secret,
expiresIn, expiresIn,
}), }),
@ -164,7 +164,7 @@ export class TokenService {
}; };
return { return {
token: this.jwtService.sign(jwtPayload, { token: this.jwtWrapperService.sign(jwtPayload, {
secret, secret,
expiresIn, expiresIn,
}), }),
@ -193,7 +193,7 @@ export class TokenService {
} else { } else {
expiresIn = this.environmentService.get('API_TOKEN_EXPIRES_IN'); expiresIn = this.environmentService.get('API_TOKEN_EXPIRES_IN');
} }
const token = this.jwtService.sign(jwtPayload, { const token = this.jwtWrapperService.sign(jwtPayload, {
secret, secret,
expiresIn, expiresIn,
jwtid: apiKeyId, jwtid: apiKeyId,
@ -496,7 +496,10 @@ export class TokenService {
async verifyJwt(token: string, secret?: string) { async verifyJwt(token: string, secret?: string) {
try { try {
return this.jwtService.verify(token, secret ? { secret } : undefined); return this.jwtWrapperService.verify(
token,
secret ? { secret } : undefined,
);
} catch (error) { } catch (error) {
if (error instanceof TokenExpiredError) { if (error instanceof TokenExpiredError) {
throw new UnauthorizedException('Token has expired.'); throw new UnauthorizedException('Token has expired.');
@ -668,12 +671,4 @@ export class TokenService {
return { success: true }; return { success: true };
} }
async encodePayload(payload: any, options?: any): Promise<string> {
return this.jwtService.sign(payload, options);
}
async decodePayload(payload: any, options?: any): Promise<string> {
return this.jwtService.decode(payload, options);
}
} }

View File

@ -1,15 +1,15 @@
import { forwardRef, Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module'; import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
import { FilePathGuard } from 'src/engine/core-modules/file/guards/file-path-guard'; import { FilePathGuard } from 'src/engine/core-modules/file/guards/file-path-guard';
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { FileController } from './controllers/file.controller'; import { FileController } from './controllers/file.controller';
import { FileService } from './services/file.service'; import { FileService } from './services/file.service';
@Module({ @Module({
imports: [FileUploadModule, forwardRef(() => AuthModule)], imports: [FileUploadModule, JwtModule],
providers: [FileService, EnvironmentService, FilePathGuard], providers: [FileService, EnvironmentService, FilePathGuard],
exports: [FileService], exports: [FileService],
controllers: [FileController], controllers: [FileController],

View File

@ -6,13 +6,13 @@ import {
Injectable, Injectable,
} from '@nestjs/common'; } from '@nestjs/common';
import { TokenService } from 'src/engine/core-modules/auth/services/token.service'; import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
@Injectable() @Injectable()
export class FilePathGuard implements CanActivate { export class FilePathGuard implements CanActivate {
constructor( constructor(
private readonly tokenService: TokenService, private readonly jwtWrapperService: JwtWrapperService,
private readonly environmentService: EnvironmentService, private readonly environmentService: EnvironmentService,
) {} ) {}
@ -22,11 +22,11 @@ export class FilePathGuard implements CanActivate {
if (query && query['token']) { if (query && query['token']) {
const payloadToDecode = query['token']; const payloadToDecode = query['token'];
const decodedPayload = await this.tokenService.decodePayload( const decodedPayload = await this.jwtWrapperService.decode(
payloadToDecode, payloadToDecode,
{ {
secret: this.environmentService.get('FILE_TOKEN_SECRET'), secret: this.environmentService.get('FILE_TOKEN_SECRET'),
}, } as any,
); );
const expirationDate = decodedPayload?.['expiration_date']; const expirationDate = decodedPayload?.['expiration_date'];

View File

@ -10,16 +10,16 @@ import {
FileStorageExceptionCode, FileStorageExceptionCode,
} from 'src/engine/integrations/file-storage/interfaces/file-storage-exception'; } from 'src/engine/integrations/file-storage/interfaces/file-storage-exception';
import { TokenService } from 'src/engine/core-modules/auth/services/token.service'; import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service'; import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
@Injectable() @Injectable()
export class FileService { export class FileService {
constructor( constructor(
private readonly jwtWrapperService: JwtWrapperService,
private readonly fileStorageService: FileStorageService, private readonly fileStorageService: FileStorageService,
private readonly environmentService: EnvironmentService, private readonly environmentService: EnvironmentService,
private readonly tokenService: TokenService,
) {} ) {}
async getFileStream( async getFileStream(
@ -57,7 +57,7 @@ export class FileService {
const expirationDate = addMilliseconds(new Date(), ms(fileTokenExpiresIn)); const expirationDate = addMilliseconds(new Date(), ms(fileTokenExpiresIn));
const signedPayload = await this.tokenService.encodePayload( const signedPayload = await this.jwtWrapperService.sign(
{ {
expiration_date: expirationDate, expiration_date: expirationDate,
...payloadToEncode, ...payloadToEncode,

View File

@ -0,0 +1,27 @@
/* eslint-disable no-restricted-imports */
import { Module } from '@nestjs/common';
import { JwtModule as NestJwtModule } from '@nestjs/jwt';
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
import { EnvironmentModule } from 'src/engine/integrations/environment/environment.module';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
const InternalJwtModule = NestJwtModule.registerAsync({
useFactory: async (environmentService: EnvironmentService) => {
return {
secret: environmentService.get('ACCESS_TOKEN_SECRET'),
signOptions: {
expiresIn: environmentService.get('ACCESS_TOKEN_EXPIRES_IN'),
},
};
},
inject: [EnvironmentService],
});
@Module({
imports: [InternalJwtModule, EnvironmentModule],
controllers: [],
providers: [JwtWrapperService],
exports: [JwtWrapperService],
})
export class JwtModule {}

View File

@ -0,0 +1,21 @@
import { Injectable } from '@nestjs/common';
import { JwtService, JwtSignOptions, JwtVerifyOptions } from '@nestjs/jwt';
import * as jwt from 'jsonwebtoken';
@Injectable()
export class JwtWrapperService {
constructor(private readonly jwtService: JwtService) {}
sign(payload: string, options?: JwtSignOptions): string {
return this.jwtService.sign(payload, options);
}
verify<T extends object = any>(token: string, options?: JwtVerifyOptions): T {
return this.jwtService.verify(token, options);
}
decode<T = any>(payload: string, options: jwt.DecodeOptions): T {
return this.jwtService.decode(payload, options);
}
}

View File

@ -70,20 +70,6 @@ export class UserResolver {
assert(user, 'User not found'); assert(user, 'User not found');
user.workspaces = await Promise.all(
user.workspaces.map(async (userWorkspace) => {
if (userWorkspace.workspace.logo) {
const workspaceLogoToken = await this.fileService.encodeFileToken({
workspace_id: userWorkspace.workspace.id,
});
userWorkspace.workspace.logo = `${userWorkspace.workspace.logo}?token=${workspaceLogoToken}`;
}
return userWorkspace;
}),
);
return user; return user;
} }
@ -186,7 +172,11 @@ export class UserResolver {
workspaceId, workspaceId,
}); });
return paths[0]; const fileToken = await this.fileService.encodeFileToken({
workspace_id: workspaceId,
});
return `${paths[0]}?token=${fileToken}`;
} }
@UseGuards(DemoEnvGuard) @UseGuards(DemoEnvGuard)

View File

@ -7,6 +7,7 @@ import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module'; import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module'; import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module'; import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
@ -17,7 +18,6 @@ import { WorkspaceResolver } from 'src/engine/core-modules/workspace/workspace.r
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module'; import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module';
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module'; import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module';
import { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts'; import { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts';
import { Workspace } from './workspace.entity'; import { Workspace } from './workspace.entity';
@ -30,6 +30,7 @@ import { WorkspaceService } from './services/workspace.service';
NestjsQueryGraphQLModule.forFeature({ NestjsQueryGraphQLModule.forFeature({
imports: [ imports: [
BillingModule, BillingModule,
FileModule,
FileUploadModule, FileUploadModule,
WorkspaceCacheVersionModule, WorkspaceCacheVersionModule,
NestjsQueryTypeOrmModule.forFeature( NestjsQueryTypeOrmModule.forFeature(

View File

@ -15,6 +15,7 @@ import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.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 { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { User } from 'src/engine/core-modules/user/user.entity'; import { User } from 'src/engine/core-modules/user/user.entity';
import { ActivateWorkspaceInput } from 'src/engine/core-modules/workspace/dtos/activate-workspace-input'; import { ActivateWorkspaceInput } from 'src/engine/core-modules/workspace/dtos/activate-workspace-input';
@ -41,6 +42,7 @@ export class WorkspaceResolver {
private readonly workspaceCacheVersionService: WorkspaceCacheVersionService, private readonly workspaceCacheVersionService: WorkspaceCacheVersionService,
private readonly userWorkspaceService: UserWorkspaceService, private readonly userWorkspaceService: UserWorkspaceService,
private readonly fileUploadService: FileUploadService, private readonly fileUploadService: FileUploadService,
private readonly fileService: FileService,
private readonly billingSubscriptionService: BillingSubscriptionService, private readonly billingSubscriptionService: BillingSubscriptionService,
) {} ) {}
@ -92,7 +94,11 @@ export class WorkspaceResolver {
logo: paths[0], logo: paths[0],
}); });
return paths[0]; const workspaceLogoToken = await this.fileService.encodeFileToken({
workspace_id: id,
});
return `${paths[0]}?token=${workspaceLogoToken}`;
} }
@UseGuards(DemoEnvGuard) @UseGuards(DemoEnvGuard)
@ -124,6 +130,19 @@ export class WorkspaceResolver {
return await this.userWorkspaceService.getUserCount(workspace.id); return await this.userWorkspaceService.getUserCount(workspace.id);
} }
@ResolveField(() => String)
async logo(@Parent() workspace: Workspace): Promise<string> {
if (workspace.logo) {
const workspaceLogoToken = await this.fileService.encodeFileToken({
workspace_id: workspace.id,
});
return `${workspace.logo}?token=${workspaceLogoToken}`;
}
return workspace.logo ?? '';
}
@Mutation(() => SendInviteLink) @Mutation(() => SendInviteLink)
async sendInviteLink( async sendInviteLink(
@Args() sendInviteLinkInput: SendInviteLinkInput, @Args() sendInviteLinkInput: SendInviteLinkInput,

View File

@ -114,6 +114,8 @@ export class WorkspaceSyncObjectMetadataIdentifiersService {
...objectMetadata, ...objectMetadata,
labelIdentifierFieldMetadataId: labelIdentifierFieldMetadataId:
labelIdentifierFieldMetadata?.id ?? null, labelIdentifierFieldMetadata?.id ?? null,
imageIdentifierFieldMetadataId:
imageIdentifierFieldMetadata?.id ?? null,
}); });
} }
} }
@ -161,9 +163,12 @@ export class WorkspaceSyncObjectMetadataIdentifiersService {
); );
} }
if (imageIdentifierFieldMetadata) { if (
imageIdentifierFieldMetadata &&
imageIdentifierFieldMetadata.type !== FieldMetadataType.TEXT
) {
throw new Error( throw new Error(
`Image identifier field for object ${objectMetadata.nameSingular} are not supported yet.`, `Image identifier field for object ${objectMetadata.nameSingular} has invalid type ${imageIdentifierFieldMetadata.type}`,
); );
} }
} }

View File

@ -39,6 +39,7 @@ import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-o
description: 'A person', description: 'A person',
icon: 'IconUser', icon: 'IconUser',
labelIdentifierStandardId: PERSON_STANDARD_FIELD_IDS.name, labelIdentifierStandardId: PERSON_STANDARD_FIELD_IDS.name,
imageIdentifierStandardId: PERSON_STANDARD_FIELD_IDS.avatarUrl,
}) })
export class PersonWorkspaceEntity extends BaseWorkspaceEntity { export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceField({ @WorkspaceField({