diff --git a/components/gitpod-db/src/team-db.spec.db.ts b/components/gitpod-db/src/team-db.spec.db.ts index 704dcbee89f395..023e1bda2d6408 100644 --- a/components/gitpod-db/src/team-db.spec.db.ts +++ b/components/gitpod-db/src/team-db.spec.db.ts @@ -22,11 +22,11 @@ import { DBIdentity } from './typeorm/entity/db-identity'; db = testContainer.get(TeamDBImpl); userDb = testContainer.get(TypeORMUserDBImpl); - async before() { + async beforeEach() { await this.wipeRepo(); } - async after() { + async afterEach() { await this.wipeRepo(); } @@ -62,6 +62,20 @@ import { DBIdentity } from './typeorm/entity/db-identity'; expect(members[0].primaryEmail).to.eq('tom@example.com'); } + @test(timeout(10000)) + public async findTeamMembersByUserAsOwner() { + const user = await this.userDb.newUser(); + user.identities.push({ authProviderId: 'GitHub', authId: '2345', authName: 'Nana', primaryEmail: 'nana@example.com' }); + await this.userDb.storeUser(user); + + const ownTeam = await this.db.createTeam(user.id, 'My Own Team'); + this.db.setTeamMemberRole('3456', ownTeam.id, 'owner'); + + const teams = await this.db.findTeamsByUserAsSoleOwner(user.id); + + expect(teams.length).to.eq(1); + expect(teams[0].id).to.eq(ownTeam.id); + } } module.exports = new TeamDBSpec() diff --git a/components/gitpod-db/src/team-db.ts b/components/gitpod-db/src/team-db.ts index 1c08e246613b69..06d658b1860672 100644 --- a/components/gitpod-db/src/team-db.ts +++ b/components/gitpod-db/src/team-db.ts @@ -11,6 +11,7 @@ export interface TeamDB { findTeamById(teamId: string): Promise; findMembersByTeam(teamId: string): Promise; findTeamsByUser(userId: string): Promise; + findTeamsByUserAsSoleOwner(userId: string): Promise; createTeam(userId: string, name: string): Promise; addMemberToTeam(userId: string, teamId: string): Promise; setTeamMemberRole(userId: string, teamId: string, role: TeamMemberRole): Promise; diff --git a/components/gitpod-db/src/typeorm/team-db-impl.ts b/components/gitpod-db/src/typeorm/team-db-impl.ts index 9e5f4d3bc36307..e72ff9d9991963 100644 --- a/components/gitpod-db/src/typeorm/team-db-impl.ts +++ b/components/gitpod-db/src/typeorm/team-db-impl.ts @@ -103,7 +103,22 @@ export class TeamDBImpl implements TeamDB { const membershipRepo = await this.getMembershipRepo(); const memberships = await membershipRepo.find({ userId, deleted: false }); const teams = await teamRepo.findByIds(memberships.map(m => m.teamId)); - return teams.filter(t => !t.deleted); + return teams.filter(t => !t.markedDeleted); + } + + public async findTeamsByUserAsSoleOwner(userId: string): Promise { + const teamRepo = await this.getTeamRepo(); + const membershipRepo = await this.getMembershipRepo(); + + // Find the memberships of this user, + // and among the memberships, get the teams where the user is the sole owner + const soleOwnedTeamIds = await membershipRepo.query(`SELECT tm_2.teamId FROM d_b_team_membership tm_1 + JOIN d_b_team_membership tm_2 ON tm_1.userId = ? AND tm_1.teamId=tm_2.teamId AND tm_1.role = 'owner' GROUP BY tm_2.teamId HAVING COUNT(tm_2.teamId) = 1;`, [userId]); + +// @ts-ignore + const teams = await teamRepo.findByIds(soleOwnedTeamIds.map(m => m.teamId)); + + return teams.filter(t => !t.markedDeleted); } public async createTeam(userId: string, name: string): Promise { diff --git a/components/server/src/user/user-deletion-service.ts b/components/server/src/user/user-deletion-service.ts index 0985b81a23bf2c..eb9fe763a3e6ec 100644 --- a/components/server/src/user/user-deletion-service.ts +++ b/components/server/src/user/user-deletion-service.ts @@ -5,7 +5,7 @@ */ import { injectable, inject } from "inversify"; -import { UserDB, WorkspaceDB, UserStorageResourcesDB, TeamDB } from '@gitpod/gitpod-db/lib'; +import { UserDB, WorkspaceDB, UserStorageResourcesDB, TeamDB, ProjectDB } from '@gitpod/gitpod-db/lib'; import { User, Workspace } from "@gitpod/gitpod-protocol"; import { StorageClient } from "../storage/storage-client"; import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; @@ -23,6 +23,7 @@ export class UserDeletionService { @inject(WorkspaceDB) protected readonly workspaceDb: WorkspaceDB; @inject(UserStorageResourcesDB) protected readonly userStorageResourcesDb: UserStorageResourcesDB; @inject(TeamDB) protected readonly teamDb: TeamDB; + @inject(ProjectDB) protected readonly projectDb: ProjectDB; @inject(StorageClient) protected readonly storageClient: StorageClient; @inject(WorkspaceManagerClientProvider) protected readonly workspaceManagerClientProvider: WorkspaceManagerClientProvider; @inject(WorkspaceDeletionService) protected readonly workspaceDeletionService: WorkspaceDeletionService; @@ -74,8 +75,12 @@ export class UserDeletionService { this.userStorageResourcesDb.deleteAllForUser(user.id), // Bucket this.deleteUserBucket(id), + // Owned teams + this.deleteTeams(id), // Team memberships this.deleteTeamMemberships(id), + // User projects + this.deleteUserProjects(id), ]); // Track the deletion Event for Analytics Purposes @@ -140,6 +145,23 @@ export class UserDeletionService { await Promise.all(teams.map(t => this.teamDb.removeMemberFromTeam(userId, t.id))); } + protected async deleteTeams(userId: string) { + const ownedTeams = await this.teamDb.findTeamsByUserAsSoleOwner(userId); + + ownedTeams.forEach(async team => { + const teamProjects = this.projectDb.findTeamProjects(team.id); + (await teamProjects).forEach(project => this.projectDb.markDeleted(project.id)); + }) + + await Promise.all(ownedTeams.map(t => this.teamDb.deleteTeam(t.id))); + } + + protected async deleteUserProjects(id: string) { + const userProjects = await this.projectDb.findUserProjects(id); + + await Promise.all(userProjects.map(project => this.projectDb.markDeleted(project.id))); + } + anonymizeWorkspace(ws: Workspace) { ws.context.title = 'deleted-title'; ws.context.normalizedContextURL = 'deleted-normalizedContextURL';