feat: multi-workspace followup (#4197)
* Seed UserWorkspace for existing demo/dev users * add workspaces field to currentUser * new token generation endpoint for switching workspace * lint fix * include dependency * requested fixes * resolver test pass * changing defaultWorkspace and workspaceMember when switching workspaces * tests fix * requested changes * delete user/workspace edge case handled * after merge * requested changes * :wq! * workspace manytoone relation * lint fix / import fix * gql codegen * Fix migrations and generateJWT * migration fix * relations fix --------- Co-authored-by: martmull <martmull@hotmail.fr>
This commit is contained in:
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@ -4,21 +4,21 @@
|
||||
"[typescript]": {
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true,
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.addMissingImports": "always"
|
||||
}
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true,
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.addMissingImports": "always"
|
||||
}
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true,
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.addMissingImports": "always"
|
||||
}
|
||||
},
|
||||
|
||||
@ -235,6 +235,7 @@ export type Mutation = {
|
||||
deleteUser: User;
|
||||
emailPasswordResetLink: EmailPasswordResetLink;
|
||||
generateApiKeyToken: ApiKeyToken;
|
||||
generateJWT: AuthTokens;
|
||||
generateTransientToken: TransientToken;
|
||||
impersonate: Verify;
|
||||
renewToken: AuthTokens;
|
||||
@ -289,6 +290,11 @@ export type MutationGenerateApiKeyTokenArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationGenerateJwtArgs = {
|
||||
workspaceId: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationImpersonateArgs = {
|
||||
userId: Scalars['String'];
|
||||
};
|
||||
@ -568,6 +574,7 @@ export type User = {
|
||||
createdAt: Scalars['DateTime'];
|
||||
defaultAvatarUrl?: Maybe<Scalars['String']>;
|
||||
defaultWorkspace: Workspace;
|
||||
defaultWorkspaceId: Scalars['String'];
|
||||
deletedAt?: Maybe<Scalars['DateTime']>;
|
||||
disabled?: Maybe<Scalars['Boolean']>;
|
||||
email: Scalars['String'];
|
||||
@ -581,6 +588,7 @@ export type User = {
|
||||
supportUserHash?: Maybe<Scalars['String']>;
|
||||
updatedAt: Scalars['DateTime'];
|
||||
workspaceMember?: Maybe<WorkspaceMember>;
|
||||
workspaces: Array<UserWorkspace>;
|
||||
};
|
||||
|
||||
export type UserEdge = {
|
||||
@ -596,6 +604,18 @@ export type UserExists = {
|
||||
exists: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type UserWorkspace = {
|
||||
__typename?: 'UserWorkspace';
|
||||
createdAt: Scalars['DateTime'];
|
||||
deletedAt?: Maybe<Scalars['DateTime']>;
|
||||
id: Scalars['ID'];
|
||||
updatedAt: Scalars['DateTime'];
|
||||
user: User;
|
||||
userId: Scalars['String'];
|
||||
workspace?: Maybe<Workspace>;
|
||||
workspaceId: Scalars['String'];
|
||||
};
|
||||
|
||||
export type ValidatePasswordResetToken = {
|
||||
__typename?: 'ValidatePasswordResetToken';
|
||||
email: Scalars['String'];
|
||||
@ -916,7 +936,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf
|
||||
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } } };
|
||||
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null } | null }> } };
|
||||
|
||||
export type ActivateWorkspaceMutationVariables = Exact<{
|
||||
input: ActivateWorkspaceInput;
|
||||
@ -1854,6 +1874,14 @@ export const GetCurrentUserDocument = gql`
|
||||
workspaceId
|
||||
}
|
||||
}
|
||||
workspaces {
|
||||
workspace {
|
||||
id
|
||||
displayName
|
||||
logo
|
||||
domainName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -36,6 +36,14 @@ export const GET_CURRENT_USER = gql`
|
||||
workspaceId
|
||||
}
|
||||
}
|
||||
workspaces {
|
||||
workspace {
|
||||
id
|
||||
displayName
|
||||
logo
|
||||
domainName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -3,6 +3,8 @@ import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { Workspace } from 'src/core/workspace/workspace.entity';
|
||||
import { UserService } from 'src/core/user/services/user.service';
|
||||
import { UserWorkspaceService } from 'src/core/user-workspace/user-workspace.service';
|
||||
import { User } from 'src/core/user/user.entity';
|
||||
|
||||
import { AuthResolver } from './auth.resolver';
|
||||
|
||||
@ -20,6 +22,10 @@ describe('AuthResolver', () => {
|
||||
provide: getRepositoryToken(Workspace, 'core'),
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(User, 'core'),
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: AuthService,
|
||||
useValue: {},
|
||||
@ -32,6 +38,10 @@ describe('AuthResolver', () => {
|
||||
provide: UserService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: UserWorkspaceService,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
|
||||
@ -25,6 +25,8 @@ import { UpdatePasswordViaResetTokenInput } from 'src/core/auth/dto/update-passw
|
||||
import { EmailPasswordResetLink } from 'src/core/auth/dto/email-password-reset-link.entity';
|
||||
import { InvalidatePassword } from 'src/core/auth/dto/invalidate-password.entity';
|
||||
import { EmailPasswordResetLinkInput } from 'src/core/auth/dto/email-password-reset-link.input';
|
||||
import { GenerateJwtInput } from 'src/core/auth/dto/generate-jwt.input';
|
||||
import { UserWorkspaceService } from 'src/core/user-workspace/user-workspace.service';
|
||||
|
||||
import { ApiKeyToken, AuthTokens } from './dto/token.entity';
|
||||
import { TokenService } from './services/token.service';
|
||||
@ -49,6 +51,7 @@ export class AuthResolver {
|
||||
private authService: AuthService,
|
||||
private tokenService: TokenService,
|
||||
private userService: UserService,
|
||||
private userWorkspaceService: UserWorkspaceService,
|
||||
) {}
|
||||
|
||||
@Query(() => UserExists)
|
||||
@ -128,6 +131,20 @@ export class AuthResolver {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Mutation(() => AuthTokens)
|
||||
@UseGuards(JwtAuthGuard)
|
||||
async generateJWT(
|
||||
@AuthUser() user: User,
|
||||
@Args() args: GenerateJwtInput,
|
||||
): Promise<AuthTokens> {
|
||||
const token = await this.tokenService.generateSwitchWorkspaceToken(
|
||||
user,
|
||||
args.workspaceId,
|
||||
);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
@Mutation(() => AuthTokens)
|
||||
async renewToken(@Args() args: RefreshTokenInput): Promise<AuthTokens> {
|
||||
if (!args.refreshToken) {
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
import { ArgsType, Field } from '@nestjs/graphql';
|
||||
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
@ArgsType()
|
||||
export class GenerateJwtInput {
|
||||
@Field(() => String)
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
workspaceId: string;
|
||||
}
|
||||
@ -98,7 +98,7 @@ export class AuthService {
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
relations: ['defaultWorkspace'],
|
||||
relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'],
|
||||
});
|
||||
|
||||
assert(user, "This user doesn't exist", NotFoundException);
|
||||
|
||||
@ -7,6 +7,8 @@ import { RefreshToken } from 'src/core/refresh-token/refresh-token.entity';
|
||||
import { User } from 'src/core/user/user.entity';
|
||||
import { JwtAuthStrategy } from 'src/core/auth/strategies/jwt.auth.strategy';
|
||||
import { EmailService } from 'src/integrations/email/email.service';
|
||||
import { UserWorkspaceService } from 'src/core/user-workspace/user-workspace.service';
|
||||
import { Workspace } from 'src/core/workspace/workspace.entity';
|
||||
|
||||
import { TokenService } from './token.service';
|
||||
|
||||
@ -33,6 +35,10 @@ describe('TokenService', () => {
|
||||
provide: EmailService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: UserWorkspaceService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(User, 'core'),
|
||||
useValue: {},
|
||||
@ -41,6 +47,10 @@ describe('TokenService', () => {
|
||||
provide: getRepositoryToken(RefreshToken, 'core'),
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(Workspace, 'core'),
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
|
||||
@ -29,6 +29,7 @@ import { assert } from 'src/utils/assert';
|
||||
import {
|
||||
ApiKeyToken,
|
||||
AuthToken,
|
||||
AuthTokens,
|
||||
PasswordResetToken,
|
||||
} from 'src/core/auth/dto/token.entity';
|
||||
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
||||
@ -39,6 +40,8 @@ import { EmailService } from 'src/integrations/email/email.service';
|
||||
import { InvalidatePassword } from 'src/core/auth/dto/invalidate-password.entity';
|
||||
import { EmailPasswordResetLink } from 'src/core/auth/dto/email-password-reset-link.entity';
|
||||
import { JwtData } from 'src/core/auth/types/jwt-data.type';
|
||||
import { UserWorkspaceService } from 'src/core/user-workspace/user-workspace.service';
|
||||
import { Workspace } from 'src/core/workspace/workspace.entity';
|
||||
|
||||
@Injectable()
|
||||
export class TokenService {
|
||||
@ -50,10 +53,16 @@ export class TokenService {
|
||||
private readonly userRepository: Repository<User>,
|
||||
@InjectRepository(RefreshToken, 'core')
|
||||
private readonly refreshTokenRepository: Repository<RefreshToken>,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly emailService: EmailService,
|
||||
private readonly userWorkspaceService: UserWorkspaceService,
|
||||
) {}
|
||||
|
||||
async generateAccessToken(userId: string): Promise<AuthToken> {
|
||||
async generateAccessToken(
|
||||
userId: string,
|
||||
workspaceId?: string,
|
||||
): Promise<AuthToken> {
|
||||
const expiresIn = this.environmentService.getAccessTokenExpiresIn();
|
||||
|
||||
assert(expiresIn, '', InternalServerErrorException);
|
||||
@ -74,7 +83,7 @@ export class TokenService {
|
||||
|
||||
const jwtPayload: JwtPayload = {
|
||||
sub: user.id,
|
||||
workspaceId: user.defaultWorkspace.id,
|
||||
workspaceId: workspaceId ? workspaceId : user.defaultWorkspace.id,
|
||||
};
|
||||
|
||||
return {
|
||||
@ -232,6 +241,45 @@ export class TokenService {
|
||||
};
|
||||
}
|
||||
|
||||
async generateSwitchWorkspaceToken(
|
||||
user: User,
|
||||
workspaceId: string,
|
||||
): Promise<AuthTokens> {
|
||||
const userExists = await this.userRepository.findBy({ id: user.id });
|
||||
|
||||
assert(userExists, 'User not found', NotFoundException);
|
||||
|
||||
const workspace = await this.workspaceRepository.findOne({
|
||||
where: { id: workspaceId },
|
||||
relations: ['workspaceUsers'],
|
||||
});
|
||||
|
||||
assert(workspace, 'workspace doesnt exist', NotFoundException);
|
||||
|
||||
assert(
|
||||
workspace.workspaceUsers
|
||||
.map((userWorkspace) => userWorkspace.userId)
|
||||
.includes(user.id),
|
||||
'user does not belong to workspace',
|
||||
ForbiddenException,
|
||||
);
|
||||
|
||||
await this.userRepository.save({
|
||||
id: user.id,
|
||||
defaultWorkspace: workspace,
|
||||
});
|
||||
|
||||
const token = await this.generateAccessToken(user.id, workspaceId);
|
||||
const refreshToken = await this.generateRefreshToken(user.id);
|
||||
|
||||
return {
|
||||
tokens: {
|
||||
accessToken: token,
|
||||
refreshToken,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async verifyRefreshToken(refreshToken: string) {
|
||||
const secret = this.environmentService.getRefreshTokenSecret();
|
||||
const coolDown = this.environmentService.getRefreshTokenCoolDown();
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
Unique,
|
||||
UpdateDateColumn,
|
||||
@ -22,15 +23,25 @@ export class UserWorkspace {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Field(() => User)
|
||||
@ManyToOne(() => User, (user) => user.workspaces, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'userId' })
|
||||
user: User;
|
||||
|
||||
@Field({ nullable: false })
|
||||
@Column()
|
||||
userId: string;
|
||||
|
||||
@Field(() => Workspace, { nullable: true })
|
||||
@ManyToOne(() => Workspace, (workspace) => workspace.workspaceUsers, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'workspaceId' })
|
||||
workspace: Workspace;
|
||||
|
||||
@Field({ nullable: false })
|
||||
@Column()
|
||||
workspaceId: string;
|
||||
|
||||
|
||||
@ -81,14 +81,6 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
|
||||
).length;
|
||||
}
|
||||
|
||||
async findUserWorkspaces(userId: string): Promise<UserWorkspace[]> {
|
||||
return this.userWorkspaceRepository.find({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async checkUserWorkspaceExists(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
|
||||
@ -83,12 +83,27 @@ export class UserService extends TypeOrmQueryService<User> {
|
||||
}
|
||||
|
||||
async deleteUser(userId: string): Promise<User> {
|
||||
const user = await this.userRepository.findOneBy({
|
||||
id: userId,
|
||||
const user = await this.userRepository.findOne({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
relations: ['defaultWorkspace'],
|
||||
});
|
||||
|
||||
assert(user, 'User not found');
|
||||
|
||||
const dataSourceMetadata =
|
||||
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
||||
user.defaultWorkspace.id,
|
||||
);
|
||||
|
||||
const workspaceDataSource =
|
||||
await this.typeORMService.connectToDataSource(dataSourceMetadata);
|
||||
|
||||
await workspaceDataSource?.query(
|
||||
`DELETE FROM ${dataSourceMetadata.schema}."workspaceMember" WHERE "userId" = '${userId}'`,
|
||||
);
|
||||
|
||||
await this.userRepository.delete(user.id);
|
||||
|
||||
return user;
|
||||
|
||||
@ -14,6 +14,7 @@ import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
import { RefreshToken } from 'src/core/refresh-token/refresh-token.entity';
|
||||
import { Workspace } from 'src/core/workspace/workspace.entity';
|
||||
import { WorkspaceMember } from 'src/core/user/dtos/workspace-member.dto';
|
||||
import { UserWorkspace } from 'src/core/user-workspace/user-workspace.entity';
|
||||
|
||||
@Entity({ name: 'user', schema: 'core' })
|
||||
@ObjectType('User')
|
||||
@ -72,6 +73,10 @@ export class User {
|
||||
})
|
||||
defaultWorkspace: Workspace;
|
||||
|
||||
@Field()
|
||||
@Column()
|
||||
defaultWorkspaceId: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
@Column({ nullable: true })
|
||||
passwordResetToken: string;
|
||||
@ -87,4 +92,8 @@ export class User {
|
||||
|
||||
@Field(() => WorkspaceMember, { nullable: true })
|
||||
workspaceMember: WorkspaceMember;
|
||||
|
||||
@Field(() => [UserWorkspace])
|
||||
@OneToMany(() => UserWorkspace, (userWorkspace) => userWorkspace.user)
|
||||
workspaces: UserWorkspace[];
|
||||
}
|
||||
|
||||
@ -10,6 +10,8 @@ import { UserResolver } from 'src/core/user/user.resolver';
|
||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||
import { DataSourceModule } from 'src/metadata/data-source/data-source.module';
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { UserWorkspaceModule } from 'src/core/user-workspace/user-workspace.module';
|
||||
import { UserWorkspace } from 'src/core/user-workspace/user-workspace.entity';
|
||||
|
||||
import { userAutoResolverOpts } from './user.auto-resolver-opts';
|
||||
|
||||
@ -19,13 +21,14 @@ import { UserService } from './services/user.service';
|
||||
imports: [
|
||||
NestjsQueryGraphQLModule.forFeature({
|
||||
imports: [
|
||||
NestjsQueryTypeOrmModule.forFeature([User], 'core'),
|
||||
NestjsQueryTypeOrmModule.forFeature([User, UserWorkspace], 'core'),
|
||||
TypeORMModule,
|
||||
],
|
||||
resolvers: userAutoResolverOpts,
|
||||
}),
|
||||
DataSourceModule,
|
||||
FileModule,
|
||||
UserWorkspaceModule,
|
||||
],
|
||||
exports: [UserService],
|
||||
providers: [UserService, UserResolver, TypeORMService],
|
||||
|
||||
@ -23,8 +23,11 @@ import { assert } from 'src/utils/assert';
|
||||
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
|
||||
import { User } from 'src/core/user/user.entity';
|
||||
import { WorkspaceMember } from 'src/core/user/dtos/workspace-member.dto';
|
||||
import { UserWorkspaceService } from 'src/core/user-workspace/user-workspace.service';
|
||||
|
||||
import { UserService } from './services/user.service';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
const getHMACKey = (email?: string, key?: string | null) => {
|
||||
if (!email || !key) return null;
|
||||
@ -38,15 +41,21 @@ const getHMACKey = (email?: string, key?: string | null) => {
|
||||
@Resolver(() => User)
|
||||
export class UserResolver {
|
||||
constructor(
|
||||
@InjectRepository(User, 'core')
|
||||
private readonly userRepository: Repository<User>,
|
||||
private readonly userService: UserService,
|
||||
private readonly userWorkspaceService: UserWorkspaceService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly fileUploadService: FileUploadService,
|
||||
) {}
|
||||
|
||||
@Query(() => User)
|
||||
async currentUser(@AuthUser() { id }: User) {
|
||||
const user = await this.userService.findById(id, {
|
||||
relations: [{ name: 'defaultWorkspace', query: {} }],
|
||||
async currentUser(@AuthUser() { id }: User): Promise<User> {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'],
|
||||
});
|
||||
|
||||
assert(user, 'User not found');
|
||||
|
||||
@ -3,6 +3,8 @@ import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { Workspace } from 'src/core/workspace/workspace.entity';
|
||||
import { WorkspaceManagerService } from 'src/workspace/workspace-manager/workspace-manager.service';
|
||||
import { UserWorkspace } from 'src/core/user-workspace/user-workspace.entity';
|
||||
import { User } from 'src/core/user/user.entity';
|
||||
import { BillingService } from 'src/core/billing/billing.service';
|
||||
import { UserWorkspaceService } from 'src/core/user-workspace/user-workspace.service';
|
||||
|
||||
@ -19,6 +21,14 @@ describe('WorkspaceService', () => {
|
||||
provide: getRepositoryToken(Workspace, 'core'),
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(UserWorkspace, 'core'),
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(User, 'core'),
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceManagerService,
|
||||
useValue: {},
|
||||
|
||||
@ -10,6 +10,7 @@ import { WorkspaceManagerService } from 'src/workspace/workspace-manager/workspa
|
||||
import { Workspace } from 'src/core/workspace/workspace.entity';
|
||||
import { User } from 'src/core/user/user.entity';
|
||||
import { ActivateWorkspaceInput } from 'src/core/workspace/dtos/activate-workspace-input';
|
||||
import { UserWorkspace } from 'src/core/user-workspace/user-workspace.entity';
|
||||
import { UserWorkspaceService } from 'src/core/user-workspace/user-workspace.service';
|
||||
import { BillingService } from 'src/core/billing/billing.service';
|
||||
|
||||
@ -17,6 +18,10 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectRepository(UserWorkspace, 'core')
|
||||
private readonly userWorkspaceRepository: Repository<UserWorkspace>,
|
||||
@InjectRepository(User, 'core')
|
||||
private readonly userRepository: Repository<User>,
|
||||
private readonly workspaceManagerService: WorkspaceManagerService,
|
||||
private readonly userWorkspaceService: UserWorkspaceService,
|
||||
private readonly billingService: BillingService,
|
||||
@ -49,6 +54,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
||||
|
||||
assert(workspace, 'Workspace not found');
|
||||
|
||||
await this.userWorkspaceRepository.delete({ workspaceId: id });
|
||||
await this.billingService.deleteSubscription(workspace.id);
|
||||
|
||||
await this.workspaceManagerService.delete(id);
|
||||
|
||||
@ -15,6 +15,7 @@ import Stripe from 'stripe';
|
||||
import { User } from 'src/core/user/user.entity';
|
||||
import { FeatureFlagEntity } from 'src/core/feature-flag/feature-flag.entity';
|
||||
import { BillingSubscription } from 'src/core/billing/entities/billing-subscription.entity';
|
||||
import { UserWorkspace } from 'src/core/user-workspace/user-workspace.entity';
|
||||
|
||||
@Entity({ name: 'workspace', schema: 'core' })
|
||||
@ObjectType('Workspace')
|
||||
@ -55,6 +56,11 @@ export class Workspace {
|
||||
@OneToMany(() => User, (user) => user.defaultWorkspace)
|
||||
users: User[];
|
||||
|
||||
@OneToMany(() => UserWorkspace, (userWorkspace) => userWorkspace.workspace, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
workspaceUsers: UserWorkspace[];
|
||||
|
||||
@Field()
|
||||
@Column({ default: true })
|
||||
allowImpersonation: boolean;
|
||||
|
||||
@ -8,6 +8,8 @@ import { WorkspaceManagerModule } from 'src/workspace/workspace-manager/workspac
|
||||
import { WorkspaceResolver } from 'src/core/workspace/workspace.resolver';
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { FeatureFlagEntity } from 'src/core/feature-flag/feature-flag.entity';
|
||||
import { UserWorkspace } from 'src/core/user-workspace/user-workspace.entity';
|
||||
import { User } from 'src/core/user/user.entity';
|
||||
import { UserWorkspaceModule } from 'src/core/user-workspace/user-workspace.module';
|
||||
import { BillingModule } from 'src/core/billing/billing.module';
|
||||
|
||||
@ -24,7 +26,7 @@ import { WorkspaceService } from './services/workspace.service';
|
||||
BillingModule,
|
||||
FileModule,
|
||||
NestjsQueryTypeOrmModule.forFeature(
|
||||
[Workspace, FeatureFlagEntity],
|
||||
[User, Workspace, UserWorkspace, FeatureFlagEntity],
|
||||
'core',
|
||||
),
|
||||
UserWorkspaceModule,
|
||||
|
||||
@ -9,6 +9,10 @@ import {
|
||||
deleteWorkspaces,
|
||||
} from 'src/database/typeorm-seeds/core/demo/workspaces';
|
||||
import { deleteFeatureFlags } from 'src/database/typeorm-seeds/core/demo/feature-flags';
|
||||
import {
|
||||
deleteUserWorkspaces,
|
||||
seedUserWorkspaces,
|
||||
} from 'src/database/typeorm-seeds/core/demo/userWorkspaces';
|
||||
|
||||
export const seedCoreSchema = async (
|
||||
workspaceDataSource: DataSource,
|
||||
@ -18,6 +22,7 @@ export const seedCoreSchema = async (
|
||||
|
||||
await seedWorkspaces(workspaceDataSource, schemaName, workspaceId);
|
||||
await seedUsers(workspaceDataSource, schemaName, workspaceId);
|
||||
await seedUserWorkspaces(workspaceDataSource, schemaName, workspaceId);
|
||||
};
|
||||
|
||||
export const deleteCoreSchema = async (
|
||||
@ -26,6 +31,7 @@ export const deleteCoreSchema = async (
|
||||
) => {
|
||||
const schemaName = 'core';
|
||||
|
||||
await deleteUserWorkspaces(workspaceDataSource, schemaName, workspaceId);
|
||||
await deleteUsersByWorkspace(workspaceDataSource, schemaName, workspaceId);
|
||||
await deleteFeatureFlags(workspaceDataSource, schemaName, workspaceId);
|
||||
// deleteWorkspaces should be last
|
||||
|
||||
@ -0,0 +1,51 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
const tableName = 'userWorkspace';
|
||||
|
||||
export enum DemoSeedUserIds {
|
||||
Noah = '20202020-9e3b-46d4-a556-88b9ddc2b035',
|
||||
Hugo = '20202020-3957-4908-9c36-2929a23f8358',
|
||||
Julia = '20202020-7169-42cf-bc47-1cfef15264b9',
|
||||
}
|
||||
|
||||
export const seedUserWorkspaces = async (
|
||||
workspaceDataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
) => {
|
||||
await workspaceDataSource
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.${tableName}`, ['userId', 'workspaceId'])
|
||||
.orIgnore()
|
||||
.values([
|
||||
{
|
||||
userId: DemoSeedUserIds.Noah,
|
||||
workspaceId: workspaceId,
|
||||
},
|
||||
{
|
||||
userId: DemoSeedUserIds.Hugo,
|
||||
workspaceId: workspaceId,
|
||||
},
|
||||
{
|
||||
userId: DemoSeedUserIds.Julia,
|
||||
workspaceId: workspaceId,
|
||||
},
|
||||
])
|
||||
.execute();
|
||||
};
|
||||
|
||||
export const deleteUserWorkspaces = async (
|
||||
workspaceDataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
) => {
|
||||
await workspaceDataSource
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(`${schemaName}.${tableName}`)
|
||||
.where(`"${tableName}"."workspaceId" = :workspaceId`, {
|
||||
workspaceId,
|
||||
})
|
||||
.execute();
|
||||
};
|
||||
@ -12,6 +12,10 @@ import {
|
||||
seedFeatureFlags,
|
||||
deleteFeatureFlags,
|
||||
} from 'src/database/typeorm-seeds/core/feature-flags';
|
||||
import {
|
||||
deleteUserWorkspaces,
|
||||
seedUserWorkspaces,
|
||||
} from 'src/database/typeorm-seeds/core/userWorkspaces';
|
||||
|
||||
export const seedCoreSchema = async (
|
||||
workspaceDataSource: DataSource,
|
||||
@ -21,6 +25,7 @@ export const seedCoreSchema = async (
|
||||
|
||||
await seedWorkspaces(workspaceDataSource, schemaName, workspaceId);
|
||||
await seedUsers(workspaceDataSource, schemaName, workspaceId);
|
||||
await seedUserWorkspaces(workspaceDataSource, schemaName, workspaceId);
|
||||
await seedFeatureFlags(workspaceDataSource, schemaName, workspaceId);
|
||||
};
|
||||
|
||||
@ -30,6 +35,7 @@ export const deleteCoreSchema = async (
|
||||
) => {
|
||||
const schemaName = 'core';
|
||||
|
||||
await deleteUserWorkspaces(workspaceDataSource, schemaName, workspaceId);
|
||||
await deleteUsersByWorkspace(workspaceDataSource, schemaName, workspaceId);
|
||||
await deleteFeatureFlags(workspaceDataSource, schemaName, workspaceId);
|
||||
// deleteWorkspaces should be last
|
||||
|
||||
@ -0,0 +1,53 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
// import { SeedWorkspaceId } from 'src/database/typeorm-seeds/core/workspaces';
|
||||
|
||||
const tableName = 'userWorkspace';
|
||||
|
||||
export enum SeedUserIds {
|
||||
Tim = '20202020-9e3b-46d4-a556-88b9ddc2b034',
|
||||
Jony = '20202020-3957-4908-9c36-2929a23f8357',
|
||||
Phil = '20202020-7169-42cf-bc47-1cfef15264b8',
|
||||
}
|
||||
|
||||
export const seedUserWorkspaces = async (
|
||||
workspaceDataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
) => {
|
||||
await workspaceDataSource
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.${tableName}`, ['userId', 'workspaceId'])
|
||||
.orIgnore()
|
||||
.values([
|
||||
{
|
||||
userId: SeedUserIds.Tim,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
userId: SeedUserIds.Jony,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
userId: SeedUserIds.Phil,
|
||||
workspaceId,
|
||||
},
|
||||
])
|
||||
.execute();
|
||||
};
|
||||
|
||||
export const deleteUserWorkspaces = async (
|
||||
workspaceDataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
) => {
|
||||
await workspaceDataSource
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(`${schemaName}.${tableName}`)
|
||||
.where(`"${tableName}"."workspaceId" = :workspaceId`, {
|
||||
workspaceId,
|
||||
})
|
||||
.execute();
|
||||
};
|
||||
@ -0,0 +1,28 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class UpdateUserWorkspace1709314035408 implements MigrationInterface {
|
||||
name = 'UpdateUserWorkspace1709314035408';
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE "core"."userWorkspace"
|
||||
ADD CONSTRAINT "FK_37fdc7357af701e595c5c3a9bd6"
|
||||
FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id")
|
||||
ON DELETE CASCADE ON UPDATE NO ACTION
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE "core"."userWorkspace"
|
||||
ADD CONSTRAINT "FK_cb488f32c6a0827b938edadf221"
|
||||
FOREIGN KEY ("userId") REFERENCES "core"."user"("id")
|
||||
ON DELETE CASCADE ON UPDATE NO ACTION
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."userWorkspace" DROP CONSTRAINT "FK_cb488f32c6a0827b938edadf221"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."userWorkspace" DROP CONSTRAINT "FK_37fdc7357af701e595c5c3a9bd6"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,7 @@ import { RefreshToken } from 'src/core/refresh-token/refresh-token.entity';
|
||||
import { FeatureFlagEntity } from 'src/core/feature-flag/feature-flag.entity';
|
||||
import { BillingSubscription } from 'src/core/billing/entities/billing-subscription.entity';
|
||||
import { BillingSubscriptionItem } from 'src/core/billing/entities/billing-subscription-item.entity';
|
||||
import { UserWorkspace } from 'src/core/user-workspace/user-workspace.entity';
|
||||
|
||||
@Injectable()
|
||||
export class TypeORMService implements OnModuleInit, OnModuleDestroy {
|
||||
@ -26,6 +27,7 @@ export class TypeORMService implements OnModuleInit, OnModuleDestroy {
|
||||
entities: [
|
||||
User,
|
||||
Workspace,
|
||||
UserWorkspace,
|
||||
RefreshToken,
|
||||
FeatureFlagEntity,
|
||||
BillingSubscription,
|
||||
|
||||
Reference in New Issue
Block a user