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:
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>;
|
||||
}
|
||||
|
||||
@ -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',
|
||||
}
|
||||
@ -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],
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
Reference in New Issue
Block a user