refacto(*): remove everything about default workspace (#9157)

## Summary
- [x] Remove defaultWorkspace in user
- [x] Remove all occurrence of defaultWorkspace and defaultWorkspaceId
- [x] Improve activate workspace flow
- [x] Improve security on social login
- [x] Add `ImpersonateGuard`
- [x] Allow to use impersonation with couple `User/Workspace`
- [x] Prevent unexpected reload on activate workspace
- [x] Scope login token with workspaceId 

Fix https://github.com/twentyhq/twenty/issues/9033#event-15714863042
This commit is contained in:
Antoine Moreaux
2024-12-24 12:47:41 +01:00
committed by GitHub
parent fe6948ba0b
commit cd2946b670
78 changed files with 1150 additions and 1244 deletions

View File

@ -72,18 +72,13 @@ export class UserService extends TypeOrmQueryService<User> {
return workspaceMemberRepository.find();
}
async deleteUser(userId: string): Promise<User> {
const user = await this.userRepository.findOne({
where: {
id: userId,
},
relations: ['defaultWorkspace'],
});
assert(user, 'User not found');
const workspaceId = user.defaultWorkspaceId;
private async deleteUserFromWorkspace({
userId,
workspaceId,
}: {
userId: string;
workspaceId: string;
}) {
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
workspaceId,
@ -103,8 +98,6 @@ export class UserService extends TypeOrmQueryService<User> {
if (workspaceMembers.length === 1) {
await this.workspaceService.deleteWorkspace(workspaceId);
return user;
}
await workspaceDataSource?.query(
@ -131,6 +124,19 @@ export class UserService extends TypeOrmQueryService<User> {
],
workspaceId,
});
}
async deleteUser(userId: string): Promise<User> {
const user = await this.userRepository.findOne({
where: {
id: userId,
},
relations: ['workspaces'],
});
userValidator.assertIsDefinedOrThrow(user);
await Promise.all(user.workspaces.map(this.deleteUserFromWorkspace));
return user;
}
@ -154,16 +160,4 @@ export class UserService extends TypeOrmQueryService<User> {
),
);
}
async saveDefaultWorkspaceIfUserHasAccessOrThrow(
userId: string,
workspaceId: string,
) {
await this.hasUserAccessToWorkspaceOrThrow(userId, workspaceId);
return await this.userRepository.save({
id: userId,
defaultWorkspaceId: workspaceId,
});
}
}

View File

@ -6,7 +6,6 @@ import {
CreateDateColumn,
Entity,
Index,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
Relation,
@ -81,16 +80,6 @@ export class User {
@Column({ nullable: true, type: 'timestamptz' })
deletedAt: Date;
@Field(() => Workspace, { nullable: false })
@ManyToOne(() => Workspace, (workspace) => workspace.users, {
onDelete: 'RESTRICT',
})
defaultWorkspace: Relation<Workspace>;
@Field()
@Column()
defaultWorkspaceId: string;
@OneToMany(() => AppToken, (appToken) => appToken.user, {
cascade: true,
})
@ -110,4 +99,7 @@ export class User {
@Field(() => OnboardingStatus, { nullable: true })
onboardingStatus: OnboardingStatus;
@Field(() => Workspace, { nullable: true })
currentWorkspace: Relation<Workspace>;
}

View File

@ -0,0 +1,11 @@
import { CustomException } from 'src/utils/custom-exception';
export class UserException extends CustomException {
constructor(message: string, code: UserExceptionCode) {
super(message, code);
}
}
export enum UserExceptionCode {
USER_NOT_FOUND = 'USER_NOT_FOUND',
}

View File

@ -18,6 +18,7 @@ import { UserResolver } from 'src/engine/core-modules/user/user.resolver';
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { userAutoResolverOpts } from './user.auto-resolver-opts';
@ -41,6 +42,7 @@ import { UserService } from './services/user.service';
TypeOrmModule.forFeature([KeyValuePair], 'core'),
UserVarsModule,
AnalyticsModule,
DomainManagerModule,
],
exports: [UserService],
providers: [UserService, UserResolver, TypeORMService],

View File

@ -44,6 +44,9 @@ import {
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { userValidator } from 'src/engine/core-modules/user/user.validate';
import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
const getHMACKey = (email?: string, key?: string | null) => {
if (!email || !key) return null;
@ -66,28 +69,31 @@ export class UserResolver {
private readonly userVarService: UserVarsService,
private readonly fileService: FileService,
private readonly analyticsService: AnalyticsService,
private readonly domainManagerService: DomainManagerService,
) {}
@Query(() => User)
async currentUser(
@AuthUser() { id: userId }: User,
@AuthWorkspace() { id: workspaceId }: Workspace,
@OriginHeader() origin: string,
): Promise<User> {
if (
this.environmentService.get('IS_MULTIWORKSPACE_ENABLED') &&
workspaceId
) {
await this.userService.saveDefaultWorkspaceIfUserHasAccessOrThrow(
userId,
workspaceId,
const workspace =
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
origin,
);
}
workspaceValidator.assertIsDefinedOrThrow(workspace);
await this.userService.hasUserAccessToWorkspaceOrThrow(
userId,
workspace.id,
);
const user = await this.userRepository.findOne({
where: {
id: userId,
},
relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'],
relations: ['workspaces', 'workspaces.workspace'],
});
userValidator.assertIsDefinedOrThrow(
@ -95,14 +101,17 @@ export class UserResolver {
new AuthException('User not found', AuthExceptionCode.USER_NOT_FOUND),
);
return user;
return { ...user, currentWorkspace: workspace };
}
@ResolveField(() => GraphQLJSONObject)
async userVars(@Parent() user: User): Promise<Record<string, any>> {
async userVars(
@Parent() user: User,
@AuthWorkspace() workspace: Workspace,
): Promise<Record<string, any>> {
const userVars = await this.userVarService.getAll({
userId: user.id,
workspaceId: user.defaultWorkspaceId,
workspaceId: workspace.id,
});
const userVarAllowList = [
@ -127,13 +136,13 @@ export class UserResolver {
): Promise<WorkspaceMember | null> {
const workspaceMember = await this.userService.loadWorkspaceMember(
user,
workspace ?? user.defaultWorkspace,
workspace,
);
if (workspaceMember && workspaceMember.avatarUrl) {
const avatarUrlToken = await this.fileService.encodeFileToken({
workspaceMemberId: workspaceMember.id,
workspaceId: user.defaultWorkspaceId,
workspaceId: workspace.id,
});
workspaceMember.avatarUrl = `${workspaceMember.avatarUrl}?token=${avatarUrlToken}`;
@ -146,16 +155,18 @@ export class UserResolver {
@ResolveField(() => [WorkspaceMember], {
nullable: true,
})
async workspaceMembers(@Parent() user: User): Promise<WorkspaceMember[]> {
const workspaceMembers = await this.userService.loadWorkspaceMembers(
user.defaultWorkspace,
);
async workspaceMembers(
@Parent() user: User,
@AuthWorkspace() workspace: Workspace,
): Promise<WorkspaceMember[]> {
const workspaceMembers =
await this.userService.loadWorkspaceMembers(workspace);
for (const workspaceMember of workspaceMembers) {
if (workspaceMember.avatarUrl) {
const avatarUrlToken = await this.fileService.encodeFileToken({
workspaceMemberId: workspaceMember.id,
workspaceId: user.defaultWorkspaceId,
workspaceId: workspace.id,
});
workspaceMember.avatarUrl = `${workspaceMember.avatarUrl}?token=${avatarUrlToken}`;
@ -221,7 +232,17 @@ export class UserResolver {
}
@ResolveField(() => OnboardingStatus)
async onboardingStatus(@Parent() user: User): Promise<OnboardingStatus> {
return this.onboardingService.getOnboardingStatus(user);
async onboardingStatus(
@Parent() user: User,
@OriginHeader() origin: string,
): Promise<OnboardingStatus> {
const workspace =
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
origin,
);
workspaceValidator.assertIsDefinedOrThrow(workspace);
return this.onboardingService.getOnboardingStatus(user, workspace);
}
}

View File

@ -1,10 +1,17 @@
import { User } from 'src/engine/core-modules/user/user.entity';
import { CustomException } from 'src/utils/custom-exception';
import { isDefined } from 'src/utils/is-defined';
import {
UserException,
UserExceptionCode,
} from 'src/engine/core-modules/user/user.exception';
const assertIsDefinedOrThrow = (
user: User | undefined | null,
exceptionToThrow: CustomException,
exceptionToThrow: CustomException = new UserException(
'User not found',
UserExceptionCode.USER_NOT_FOUND,
),
): asserts user is User => {
if (!isDefined(user)) {
throw exceptionToThrow;