feat: v2 managed organizations (#19341)

* refactor: allow non unique PlatformBilling customerId

* feat: add PlatformBilling managed organizations fields

* feat: ManagedOrganization table

* refactor: bill overdue based on teamId

* refactor: restructure organizations module

* wip: organizations endpoints

* Revert "wip: organizations endpoints"

This reverts commit 0e9e66fc74f31da436f12930c1b6597c650ed621.

* refactor: unique index for managed organizations

* wip: organizations endpoints

* wip: create managed organization

* remove unecessary membership check because we have guard

* feat: create managed org

* feat: get managed org and orgs

* feat: update and delete managed orgs

* wip: api key logic

* feat: allow variable api key length

* feat: refresh managed org api key

* feat: create managed org OAuth clients using api key

* finish merge main

* chore: bump platform libraries

* tests: fix and add more tests to api-auth.strategy.e2e

* refactor: dont request managed org slug and handle metadata as object

* revert: billing service and repository update overdue based on sub and customer ids

* refactor: v2 OAuth client permissions (#19501)

* refactor: v2 permissions as string array

* refactor: frontend work with permissions as string

* tests: test '*' permissions

* fix:managed org creator have profile & test can fetch oauth client

* fix: tests

* fix: OAuthClientCard on frontend

* fix: tests
This commit is contained in:
Lauris Skraucis authored and GitHub committed 2025-02-25 18:10:46 -07:00
1 parent e4ab8d87c4
commit a44bcafcf2
192 files changed
+9103 -619

No files matched your search

@@ -1,5 +1,5 @@
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
import { RedisService } from "@/modules/redis/redis.service";
import { createMock } from "@golevelup/ts-jest";
import { ExecutionContext } from "@nestjs/common";
@@ -1,7 +1,7 @@
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
import { PlatformPlanType } from "@/modules/billing/types";
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
import { RedisService } from "@/modules/redis/redis.service";
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
@@ -1,4 +1,4 @@
import { OrganizationsMembershipRepository } from "@/modules/organizations/repositories/organizations-membership.repository";
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
import {
Injectable,
CanActivate,
@@ -1,6 +1,6 @@
import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { OrganizationsService } from "@/modules/organizations/services/organizations.service";
import { OrganizationsService } from "@/modules/organizations/index/organizations.service";
import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
@@ -20,10 +20,10 @@ export class OrganizationRolesGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const user: UserWithProfile = request.user;
const organizationId = user ? this.usersService.getUserMainOrgId(user) : null;
const organizationId = this.getOrganizationId(context);
if (!user || !organizationId) {
throw new ForbiddenException("No organization associated with the user.");
throw new ForbiddenException("OrganizationRolesGuard - No organization associated with the user.");
}
await this.isPlatform(organizationId);
@@ -40,13 +40,25 @@ export class OrganizationRolesGuard implements CanActivate {
async isPlatform(organizationId: number) {
const isPlatform = await this.organizationsService.isPlatform(organizationId);
if (!isPlatform) {
throw new ForbiddenException("Organization is not a platform (SHP).");
throw new ForbiddenException("OrganizationRolesGuard - Organization is not a platform (SHP).");
}
}
getOrganizationId(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
const user: UserWithProfile = request.user;
const authMethodOrganizationId = request.organizationId;
if (authMethodOrganizationId) return authMethodOrganizationId;
const userOrganizationId = user ? this.usersService.getUserMainOrgId(user) : null;
return userOrganizationId;
}
isMembershipAccepted(accepted: boolean) {
if (!accepted) {
throw new ForbiddenException(`User has not accepted membership in the organization.`);
throw new ForbiddenException(
`OrganizationRolesGuard - User has not accepted membership in the organization.`
);
}
}
@@ -57,7 +69,9 @@ export class OrganizationRolesGuard implements CanActivate {
const hasRequiredRole = allowedRoles.includes(membershipRole);
if (!hasRequiredRole) {
throw new ForbiddenException(`User must have one of the roles: ${allowedRoles.join(", ")}.`);
throw new ForbiddenException(
`OrganizationRolesGuard - User must have one of the roles: ${allowedRoles.join(", ")}.`
);
}
}
}
@@ -1,4 +1,4 @@
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
import { RedisService } from "@/modules/redis/redis.service";
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
import { Request } from "express";
@@ -0,0 +1,43 @@
import { ManagedOrganizationsRepository } from "@/modules/organizations/organizations/managed-organizations.repository";
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
NotFoundException,
} from "@nestjs/common";
import { Request } from "express";
import { Team } from "@calcom/prisma/client";
@Injectable()
export class IsManagedOrgInManagerOrg implements CanActivate {
constructor(private managedOrganizationsRepository: ManagedOrganizationsRepository) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request & { managedOrganization: Team }>();
const managedOrgId: string = request.params.managedOrganizationId;
const managerOrgId: string = request.params.orgId;
if (!managerOrgId) {
throw new ForbiddenException("No manager org id found in request params.");
}
if (!managedOrgId) {
throw new ForbiddenException("No managed org id found in request params.");
}
const managedOrganization = await this.managedOrganizationsRepository.getByManagerManagedIds(
Number(managerOrgId),
Number(managedOrgId)
);
if (!managedOrganization) {
throw new NotFoundException(
`Managed organization with id=${managedOrgId} within manager organization with id=${managerOrgId} not found.`
);
}
return true;
}
}
@@ -1,4 +1,4 @@
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
import { RedisService } from "@/modules/redis/redis.service";
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
import { Request } from "express";
@@ -1,5 +1,5 @@
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
import { OrganizationsWebhooksRepository } from "@/modules/organizations/repositories/organizations-webhooks.repository";
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
import { OrganizationsWebhooksRepository } from "@/modules/organizations/webhooks/organizations-webhooks.repository";
import { RedisService } from "@/modules/redis/redis.service";
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
import { Request } from "express";
@@ -1,4 +1,4 @@
import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository";
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
import {
Injectable,
CanActivate,
@@ -1,4 +1,4 @@
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
import { Request } from "express";
@@ -1,4 +1,4 @@
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
import { Request } from "express";