-
Notifications
You must be signed in to change notification settings - Fork 8.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* --init * add is webhook in org guard * -- * doc * e2e * --feedback * -- * fix typo * delete unnecessary file * fix v2 e2e * fix v2 e2e * fix e2e * fix v2 e2e --------- Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
- Loading branch information
Showing
9 changed files
with
922 additions
and
5 deletions.
There are no files selected for viewing
67 changes: 67 additions & 0 deletions
67
apps/api/v2/src/modules/auth/guards/organizations/is-webhook-in-org.guard.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository"; | ||
import { OrganizationsWebhooksRepository } from "@/modules/organizations/repositories/organizations-webhooks.repository"; | ||
import { RedisService } from "@/modules/redis/redis.service"; | ||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common"; | ||
import { Request } from "express"; | ||
|
||
import { Team } from "@calcom/prisma/client"; | ||
|
||
type CachedData = { | ||
org?: Team; | ||
canAccess?: boolean; | ||
}; | ||
|
||
@Injectable() | ||
export class IsWebhookInOrg implements CanActivate { | ||
constructor( | ||
private organizationsRepository: OrganizationsRepository, | ||
private organizationsWebhooksRepository: OrganizationsWebhooksRepository, | ||
private readonly redisService: RedisService | ||
) {} | ||
|
||
async canActivate(context: ExecutionContext): Promise<boolean> { | ||
let canAccess = false; | ||
const request = context.switchToHttp().getRequest<Request & { organization: Team }>(); | ||
const webhookId: string = request.params.webhookId; | ||
const organizationId: string = request.params.orgId; | ||
|
||
if (!organizationId) { | ||
throw new ForbiddenException("No organization id found in request params."); | ||
} | ||
if (!webhookId) { | ||
throw new ForbiddenException("No webhook id found in request params."); | ||
} | ||
|
||
const REDIS_CACHE_KEY = `apiv2:org:${webhookId}:guard:isWebhookInOrg`; | ||
const cachedData = await this.redisService.redis.get(REDIS_CACHE_KEY); | ||
|
||
if (cachedData) { | ||
const { org: cachedOrg, canAccess: cachedCanAccess } = JSON.parse(cachedData) as CachedData; | ||
if (cachedOrg?.id === Number(organizationId) && cachedCanAccess !== undefined) { | ||
request.organization = cachedOrg; | ||
return cachedCanAccess; | ||
} | ||
} | ||
|
||
const org = await this.organizationsRepository.findById(Number(organizationId)); | ||
|
||
if (org?.isOrganization) { | ||
const isWebhookInOrg = await this.organizationsWebhooksRepository.findWebhook( | ||
Number(organizationId), | ||
webhookId | ||
); | ||
if (isWebhookInOrg) canAccess = true; | ||
} | ||
|
||
if (org) { | ||
await this.redisService.redis.set( | ||
REDIS_CACHE_KEY, | ||
JSON.stringify({ org: org, canAccess } satisfies CachedData), | ||
"EX", | ||
300 | ||
); | ||
} | ||
|
||
return canAccess; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
147 changes: 147 additions & 0 deletions
147
...pi/v2/src/modules/organizations/controllers/webhooks/organizations-webhooks.controller.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
import { API_VERSIONS_VALUES } from "@/lib/api-versions"; | ||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; | ||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; | ||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; | ||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; | ||
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; | ||
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; | ||
import { IsWebhookInOrg } from "@/modules/auth/guards/organizations/is-webhook-in-org.guard"; | ||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; | ||
import { OrganizationsWebhooksService } from "@/modules/organizations/services/organizations-webhooks.service"; | ||
import { CreateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input"; | ||
import { UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input"; | ||
import { | ||
TeamWebhookOutputDto as OrgWebhookOutputDto, | ||
TeamWebhookOutputResponseDto as OrgWebhookOutputResponseDto, | ||
TeamWebhooksOutputResponseDto as OrgWebhooksOutputResponseDto, | ||
} from "@/modules/webhooks/outputs/team-webhook.output"; | ||
import { PartialWebhookInputPipe, WebhookInputPipe } from "@/modules/webhooks/pipes/WebhookInputPipe"; | ||
import { WebhookOutputPipe } from "@/modules/webhooks/pipes/WebhookOutputPipe"; | ||
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service"; | ||
import { | ||
Controller, | ||
UseGuards, | ||
Get, | ||
Param, | ||
ParseIntPipe, | ||
Query, | ||
Delete, | ||
Patch, | ||
Post, | ||
Body, | ||
HttpCode, | ||
HttpStatus, | ||
} from "@nestjs/common"; | ||
import { ApiTags as DocsTags } from "@nestjs/swagger"; | ||
import { plainToClass } from "class-transformer"; | ||
|
||
import { SUCCESS_STATUS } from "@calcom/platform-constants"; | ||
import { SkipTakePagination } from "@calcom/platform-types"; | ||
|
||
@Controller({ | ||
path: "/v2/organizations/:orgId/webhooks", | ||
version: API_VERSIONS_VALUES, | ||
}) | ||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard) | ||
@DocsTags("Organizations Webhooks") | ||
export class OrganizationsWebhooksController { | ||
constructor( | ||
private organizationsWebhooksService: OrganizationsWebhooksService, | ||
private webhooksService: WebhooksService | ||
) {} | ||
|
||
@Roles("ORG_ADMIN") | ||
@PlatformPlan("ESSENTIALS") | ||
@Get("/") | ||
@HttpCode(HttpStatus.OK) | ||
async getAllOrganizationWebhooks( | ||
@Param("orgId", ParseIntPipe) orgId: number, | ||
@Query() queryParams: SkipTakePagination | ||
): Promise<OrgWebhooksOutputResponseDto> { | ||
const { skip, take } = queryParams; | ||
const webhooks = await this.organizationsWebhooksService.getWebhooksPaginated( | ||
orgId, | ||
skip ?? 0, | ||
take ?? 250 | ||
); | ||
return { | ||
status: SUCCESS_STATUS, | ||
data: webhooks.map((webhook) => | ||
plainToClass(OrgWebhookOutputDto, new WebhookOutputPipe().transform(webhook), { | ||
strategy: "excludeAll", | ||
}) | ||
), | ||
}; | ||
} | ||
|
||
@Roles("ORG_ADMIN") | ||
@PlatformPlan("ESSENTIALS") | ||
@Post("/") | ||
@HttpCode(HttpStatus.CREATED) | ||
async createOrganizationWebhook( | ||
@Param("orgId", ParseIntPipe) orgId: number, | ||
@Body() body: CreateWebhookInputDto | ||
): Promise<OrgWebhookOutputResponseDto> { | ||
const webhook = await this.organizationsWebhooksService.createWebhook( | ||
orgId, | ||
new WebhookInputPipe().transform(body) | ||
); | ||
return { | ||
status: SUCCESS_STATUS, | ||
data: plainToClass(OrgWebhookOutputDto, new WebhookOutputPipe().transform(webhook), { | ||
strategy: "excludeAll", | ||
}), | ||
}; | ||
} | ||
|
||
@Roles("ORG_ADMIN") | ||
@PlatformPlan("ESSENTIALS") | ||
@UseGuards(IsWebhookInOrg) | ||
@Get("/:webhookId") | ||
@HttpCode(HttpStatus.OK) | ||
async getOrganizationWebhook(@Param("webhookId") webhookId: string): Promise<OrgWebhookOutputResponseDto> { | ||
const webhook = await this.organizationsWebhooksService.getWebhook(webhookId); | ||
return { | ||
status: SUCCESS_STATUS, | ||
data: plainToClass(OrgWebhookOutputDto, new WebhookOutputPipe().transform(webhook), { | ||
strategy: "excludeAll", | ||
}), | ||
}; | ||
} | ||
|
||
@Roles("ORG_ADMIN") | ||
@PlatformPlan("ESSENTIALS") | ||
@UseGuards(IsWebhookInOrg) | ||
@Delete("/:webhookId") | ||
@HttpCode(HttpStatus.OK) | ||
async deleteWebhook(@Param("webhookId") webhookId: string): Promise<OrgWebhookOutputResponseDto> { | ||
const webhook = await this.webhooksService.deleteWebhook(webhookId); | ||
return { | ||
status: SUCCESS_STATUS, | ||
data: plainToClass(OrgWebhookOutputDto, new WebhookOutputPipe().transform(webhook), { | ||
strategy: "excludeAll", | ||
}), | ||
}; | ||
} | ||
|
||
@Roles("ORG_ADMIN") | ||
@PlatformPlan("ESSENTIALS") | ||
@UseGuards(IsWebhookInOrg) | ||
@Patch("/:webhookId") | ||
@HttpCode(HttpStatus.OK) | ||
async updateOrgWebhook( | ||
@Param("webhookId") webhookId: string, | ||
@Body() body: UpdateWebhookInputDto | ||
): Promise<OrgWebhookOutputResponseDto> { | ||
const webhook = await this.organizationsWebhooksService.updateWebhook( | ||
webhookId, | ||
new PartialWebhookInputPipe().transform(body) | ||
); | ||
return { | ||
status: SUCCESS_STATUS, | ||
data: plainToClass(OrgWebhookOutputDto, new WebhookOutputPipe().transform(webhook), { | ||
strategy: "excludeAll", | ||
}), | ||
}; | ||
} | ||
} |
Oops, something went wrong.