Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): Add endpoint to fetch all workspace invitations for a user #586

Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
meta {
name: Get all invitations of user to workspaces
type: http
seq: 3
}

get {
url: {{BASE_URL}}/api/workspace/invitations?page=0&limit=10
body: none
auth: bearer
}

auth:bearer {
token: {{JWT}}
}

docs {
## Description

Fetches all the workspaces where the user is invited to.
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { IsNotEmpty, IsOptional, IsString, Matches } from 'class-validator'

Check warning on line 1 in apps/api/src/environment/dto/create.environment/create.environment.ts

View workflow job for this annotation

GitHub Actions / Validate API

'Matches' is defined but never used

export class CreateEnvironment {
@IsString()
@IsNotEmpty()
@Matches(/^[a-zA-Z0-9-_]{1,64}$/)
name: string

@IsString()
Expand Down
5 changes: 3 additions & 2 deletions apps/api/src/environment/environment.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { UserModule } from '@/user/user.module'
import { UserService } from '@/user/service/user.service'
import { QueryTransformPipe } from '@/common/pipes/query.transform.pipe'
import { fetchEvents } from '@/common/event'
import { ValidationPipe } from '@nestjs/common'

describe('Environment Controller Tests', () => {
let app: NestFastifyApplication
Expand Down Expand Up @@ -65,7 +66,7 @@ describe('Environment Controller Tests', () => {
environmentService = moduleRef.get(EnvironmentService)
userService = moduleRef.get(UserService)

app.useGlobalPipes(new QueryTransformPipe())
app.useGlobalPipes(new ValidationPipe(), new QueryTransformPipe())

await app.init()
await app.getHttpAdapter().getInstance().ready()
Expand Down Expand Up @@ -184,7 +185,7 @@ describe('Environment Controller Tests', () => {
'x-e2e-user-email': user1.email
}
})

expect(response.statusCode).toBe(400)
expect(response.json().message).toContain('name should not be empty')
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "WorkspaceMember" ADD COLUMN "createdOn" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
1 change: 1 addition & 0 deletions apps/api/src/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ model WorkspaceMember {
workspaceId String
invitationAccepted Boolean @default(false)
roles WorkspaceMemberRoleAssociation[]
createdOn DateTime @default(now())

@@unique([workspaceId, userId])
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -879,11 +879,14 @@ export class WorkspaceMembershipService {
roleSet.add(role)
}

const invitedOn = new Date()

// Create the workspace membership
const createMembership = this.prisma.workspaceMember.create({
data: {
workspaceId: workspace.id,
userId,
createdOn: invitedOn,
roles: {
create: Array.from(roleSet).map((role) => ({
role: {
Expand All @@ -904,7 +907,7 @@ export class WorkspaceMembershipService {
workspace.name,
`${process.env.WORKSPACE_FRONTEND_URL}/workspace/${workspace.slug}/join`,
currentUser.name,
new Date().toISOString(),
invitedOn.toISOString(),
true
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,8 @@ describe('Workspace Membership Controller Tests', () => {
id: expect.any(String),
userId: user2.id,
workspaceId: workspace1.id,
invitationAccepted: false
invitationAccepted: false,
createdOn: expect.any(Date)
})
})

Expand Down Expand Up @@ -909,7 +910,8 @@ describe('Workspace Membership Controller Tests', () => {
id: expect.any(String),
userId: user2.id,
workspaceId: workspace1.id,
invitationAccepted: true
invitationAccepted: true,
createdOn: expect.any(Date)
})
})

Expand Down
20 changes: 20 additions & 0 deletions apps/api/src/workspace/controller/workspace.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,26 @@ export class WorkspaceController {
return this.workspaceService.deleteWorkspace(user, workspaceSlug)
}

@Get('invitations')
@RequiredApiKeyAuthorities(Authority.READ_WORKSPACE)
async getAllInvitationsOfUser(
@CurrentUser() user: User,
@Query('page') page: number = 0,
@Query('limit') limit: number = 10,
@Query('sort') sort: string = 'name',
@Query('order') order: string = 'asc',
@Query('search') search: string = ''
) {
return this.workspaceService.getAllWorkspaceInvitations(
user,
page,
limit,
sort,
order,
search
)
}

@Get(':workspaceSlug')
@RequiredApiKeyAuthorities(Authority.READ_WORKSPACE)
async getWorkspace(
Expand Down
105 changes: 105 additions & 0 deletions apps/api/src/workspace/service/workspace.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,111 @@ export class WorkspaceService {
return { projects, environments, secrets, variables }
}

/**
* Gets all the invitations a user has to various workspaces, paginated.
* @param user The user to get the workspaces for
* @param page The page number to get
* @param limit The number of items per page to get
* @param sort The field to sort by
* @param order The order to sort in
* @param search The search string to filter by
* @returns The workspace invitations of the user, paginated, with metadata
*/
async getAllWorkspaceInvitations(
user: User,
page: number,
limit: number,
sort: string,
order: string,
search: string
) {
// fetch all workspaces of user where they are not admin
const items = await this.prisma.workspaceMember.findMany({
skip: page * limit,
take: limitMaxItemsPerPage(Number(limit)),
orderBy: {
workspace: {
[sort]: order
}
},
where: {
userId: user.id,
invitationAccepted: false,
workspace: {
name: {
contains: search
}
},
roles: {
rajdip-b marked this conversation as resolved.
Show resolved Hide resolved
none: {
role: {
hasAdminAuthority: true
}
}
}
},
select: {
workspace: {
select: {
id: true,
name: true,
slug: true,
icon: true
}
},
roles: {
select: {
role: {
select: {
name: true,
colorCode: true
}
}
}
},
createdOn: true
}
})

// get total count of workspaces of the user
const totalCount = await this.prisma.workspaceMember.count({
where: {
userId: user.id,
invitationAccepted: false,
workspace: {
name: {
contains: search
}
},
roles: {
none: {
role: {
hasAdminAuthority: true
}
}
}
}
})

//calculate metadata for pagination
const metadata = paginate(totalCount, `/workspace/invitations`, {
page,
limit: limitMaxItemsPerPage(limit),
sort,
order,
search
})

return {
items: items.map((item) => ({
...item,
invitedOn: item.createdOn,
createdOn: undefined
})),
metadata
}
}

/**
* Gets a list of project IDs that the user has access to READ.
* The user has access to a project if the project is global or if the user has the READ_PROJECT authority.
Expand Down
Loading
Loading