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
2025-02-25 18:10:46 -07:00
committed by GitHub
parent e4ab8d87c4
commit a44bcafcf2
192 changed files with 9103 additions and 619 deletions
@@ -0,0 +1,80 @@
import { hashAPIKey, stripApiKey } from "@/lib/api-key";
import { AuthMethods } from "@/lib/enums/auth-methods";
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
import { CreateApiKeyInput } from "@/modules/api-keys/inputs/create-api-key.input";
import { RefreshApiKeyInput } from "@/modules/api-keys/inputs/refresh-api-key.input";
import { ApiAuthGuardRequest } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
import { BadRequestException, Injectable, UnauthorizedException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { DateTime } from "luxon";
import { createApiKeyHandler } from "@calcom/platform-libraries";
@Injectable()
export class ApiKeysService {
constructor(
private readonly apiKeysRepository: ApiKeysRepository,
private readonly config: ConfigService
) {}
async getRequestApiKey(request: ApiAuthGuardRequest) {
if (request.authMethod !== AuthMethods.API_KEY) {
throw new UnauthorizedException(
"ApiKeysService - This endpoint can only be accessed using an API key by providing 'Authorization: Bearer <apiKey>' header"
);
}
const apiKey = request.get("Authorization")?.replace("Bearer ", "");
if (!apiKey) {
throw new UnauthorizedException("ApiKeysService - No API key provided");
}
return apiKey;
}
async createApiKey(authUserId: number, createApiKeyInput: CreateApiKeyInput) {
if (createApiKeyInput.apiKeyDaysValid && createApiKeyInput.apiKeyNeverExpires) {
throw new BadRequestException(
"ApiKeysService -Cannot set both apiKeyDaysValid and apiKeyNeverExpires. It has to be either or none of them."
);
}
const defaultApiKeyDaysValid = 30;
const apiKeyExpiresAfterDays = createApiKeyInput.apiKeyDaysValid
? createApiKeyInput.apiKeyDaysValid
: defaultApiKeyDaysValid;
const apiKeyExpiresAt = DateTime.utc().plus({ days: apiKeyExpiresAfterDays }).toJSDate();
const apiKey = await createApiKeyHandler({
ctx: {
user: {
id: authUserId,
},
},
input: {
note: createApiKeyInput.note,
neverExpires: !!createApiKeyInput.apiKeyNeverExpires,
expiresAt: apiKeyExpiresAt,
teamId: createApiKeyInput.teamId,
},
});
return apiKey;
}
async refreshApiKey(authUserId: number, apiKey: string, refreshApiKeyInput: RefreshApiKeyInput) {
const strippedApiKey = stripApiKey(apiKey, this.config.get<string>("api.keyPrefix"));
const apiKeyHash = hashAPIKey(strippedApiKey);
const apiKeyInDb = await this.apiKeysRepository.getApiKeyFromHash(apiKeyHash);
if (!apiKeyInDb) {
throw new UnauthorizedException("ApiKeysService - provided api key is not valid.");
}
const newApiKey = await this.createApiKey(authUserId, {
...refreshApiKeyInput,
note: apiKeyInDb.note || undefined,
teamId: apiKeyInDb.teamId || undefined,
});
await this.apiKeysRepository.deleteById(apiKeyInDb.id);
return newApiKey;
}
}