Skip to content

Commit

Permalink
feat: usage base billed - license key check - move to private api (ca…
Browse files Browse the repository at this point in the history
…lcom#15222)

Co-authored-by: Omar López <[email protected]>
  • Loading branch information
sean-brydon and zomars authored Jul 25, 2024
1 parent 53c37a9 commit e16662e
Show file tree
Hide file tree
Showing 16 changed files with 586 additions and 59 deletions.
6 changes: 3 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
# - You can not repackage or sell the codebase
# - Acquire a commercial license to remove these terms by visiting: cal.com/sales
#
# To enable enterprise-only features, as an admin, go to /auth/setup to select your license and follow
# instructions. This environment variable is deprecated although still supported for backward compatibility.
# @see https://console.cal.com

# To enable enterprise-only features please add your envrionment variable to the .env file then make your way to /auth/setup to select your license and follow instructions.
CALCOM_LICENSE_KEY=
CALCOM_PRIVATE_API_ROUTE="https://goblin.cal.com"
# ***********************************************************************************************************

# - DATABASE ************************************************************************************************
Expand Down
9 changes: 6 additions & 3 deletions apps/api/v1/lib/helpers/verifyApiKey.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { NextMiddleware } from "next-api-middleware";

import LicenseKeyService from "@calcom/ee/common/server/LicenseKeyService";
import { hashAPIKey } from "@calcom/features/ee/api-keys/lib/apiKeys";
import checkLicense from "@calcom/features/ee/common/server/checkLicense";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import prisma from "@calcom/prisma";

Expand All @@ -18,9 +18,12 @@ export const dateNotInPast = function (date: Date) {

// This verifies the apiKey and sets the user if it is valid.
export const verifyApiKey: NextMiddleware = async (req, res, next) => {
const hasValidLicense = await checkLicense(prisma);
if (!hasValidLicense && IS_PRODUCTION)
const licenseKeyService = await LicenseKeyService.create();
const hasValidLicense = await licenseKeyService.checkLicense();

if (!hasValidLicense && IS_PRODUCTION) {
return res.status(401).json({ error: "Invalid or missing CALCOM_LICENSE_KEY environment variable" });
}
// Check if the apiKey query param is provided.
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
// remove the prefix from the user provided api_key. If no env set default to "cal_"
Expand Down
48 changes: 27 additions & 21 deletions apps/api/v1/test/lib/middleware/verifyApiKey.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import prismaMock from "../../../../../../tests/libs/__mocks__/prismaMock";
import type { Request, Response } from "express";
import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, vi, it, expect, afterEach } from "vitest";
import { describe, vi, it, expect, afterEach, beforeEach } from "vitest";

import checkLicense from "@calcom/features/ee/common/server/checkLicense";
import LicenseKeyService from "@calcom/ee/common/server/LicenseKeyService";
import prisma from "@calcom/prisma";

import { isAdminGuard } from "~/lib/utils/isAdmin";
Expand All @@ -20,20 +20,21 @@ afterEach(() => {
vi.resetAllMocks();
});

vi.mock("@calcom/features/ee/common/server/checkLicense", () => {
return {
default: vi.fn(),
};
});

vi.mock("~/lib/utils/isAdmin", () => {
return {
isAdminGuard: vi.fn(),
};
});
vi.mock("@calcom/prisma");
vi.mock("~/lib/utils/isAdmin", () => ({
isAdminGuard: vi.fn(),
}));

describe("Verify API key", () => {
it("It should throw an error if the api key is not valid", async () => {
let service: LicenseKeyService;

beforeEach(async () => {
service = await LicenseKeyService.create();

vi.spyOn(service, "checkLicense");
});

it("should throw an error if the api key is not valid", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {},
Expand All @@ -43,7 +44,7 @@ describe("Verify API key", () => {
fn: verifyApiKey,
};

vi.mocked(checkLicense).mockResolvedValue(false);
vi.mocked(service.checkLicense).mockResolvedValue(false);
vi.mocked(isAdminGuard).mockResolvedValue({ isAdmin: false, scope: null });

const serverNext = vi.fn((next: void) => Promise.resolve(next));
Expand All @@ -53,9 +54,11 @@ describe("Verify API key", () => {
await middleware.fn(req, res, serverNext);

expect(middlewareSpy).toBeCalled();

expect(res.statusCode).toBe(401);
});
it("It should throw an error if no api key is provided", async () => {

it("should throw an error if no api key is provided", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {},
Expand All @@ -65,7 +68,7 @@ describe("Verify API key", () => {
fn: verifyApiKey,
};

vi.mocked(checkLicense).mockResolvedValue(true);
vi.mocked(service.checkLicense).mockResolvedValue(true);
vi.mocked(isAdminGuard).mockResolvedValue({ isAdmin: false, scope: null });

const serverNext = vi.fn((next: void) => Promise.resolve(next));
Expand All @@ -75,10 +78,11 @@ describe("Verify API key", () => {
await middleware.fn(req, res, serverNext);

expect(middlewareSpy).toBeCalled();

expect(res.statusCode).toBe(401);
});

it("It should set correct permissions for system-wide admin", async () => {
it("should set correct permissions for system-wide admin", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {},
Expand All @@ -97,7 +101,7 @@ describe("Verify API key", () => {
fn: verifyApiKey,
};

vi.mocked(checkLicense).mockResolvedValue(true);
vi.mocked(service.checkLicense).mockResolvedValue(true);
vi.mocked(isAdminGuard).mockResolvedValue({ isAdmin: true, scope: ScopeOfAdmin.SystemWide });

const serverNext = vi.fn((next: void) => Promise.resolve(next));
Expand All @@ -107,11 +111,12 @@ describe("Verify API key", () => {
await middleware.fn(req, res, serverNext);

expect(middlewareSpy).toBeCalled();

expect(req.isSystemWideAdmin).toBe(true);
expect(req.isOrganizationOwnerOrAdmin).toBe(false);
});

it("It should set correct permissions for org-level admin", async () => {
it("should set correct permissions for org-level admin", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {},
Expand All @@ -130,7 +135,7 @@ describe("Verify API key", () => {
fn: verifyApiKey,
};

vi.mocked(checkLicense).mockResolvedValue(true);
vi.mocked(service.checkLicense).mockResolvedValue(true);
vi.mocked(isAdminGuard).mockResolvedValue({ isAdmin: true, scope: ScopeOfAdmin.OrgOwnerOrAdmin });

const serverNext = vi.fn((next: void) => Promise.resolve(next));
Expand All @@ -140,6 +145,7 @@ describe("Verify API key", () => {
await middleware.fn(req, res, serverNext);

expect(middlewareSpy).toBeCalled();

expect(req.isSystemWideAdmin).toBe(false);
expect(req.isOrganizationOwnerOrAdmin).toBe(true);
});
Expand Down
6 changes: 3 additions & 3 deletions apps/web/pages/settings/license-key/new/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"use client";

import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import { CreateANewLicenseKeyForm } from "@calcom/features/ee/deployment/licensekey/CreateLicenseKeyForm";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { WizardLayout, Meta } from "@calcom/ui";
Expand All @@ -12,10 +11,10 @@ import PageWrapper from "@components/PageWrapper";
const CreateNewLicenseKeyPage = () => {
const { t } = useLocale();
return (
<LicenseRequired>
<>
<Meta title={t("set_up_your_organization")} description={t("organizations_description")} />
<CreateANewLicenseKeyForm />
</LicenseRequired>
</>
);
};
const LayoutWrapper = (page: React.ReactElement) => {
Expand All @@ -30,4 +29,5 @@ CreateNewLicenseKeyPage.getLayout = LayoutWrapper;
CreateNewLicenseKeyPage.PageWrapper = PageWrapper;

export default CreateNewLicenseKeyPage;

export { getServerSideProps };
5 changes: 3 additions & 2 deletions packages/features/auth/lib/getServerSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from
import type { AuthOptions, Session } from "next-auth";
import { getToken } from "next-auth/jwt";

import checkLicense from "@calcom/features/ee/common/server/checkLicense";
import LicenseKeyService from "@calcom/ee/common/server/LicenseKeyService";
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
Expand Down Expand Up @@ -65,7 +65,8 @@ export async function getServerSession(options: {
return null;
}

const hasValidLicense = await checkLicense(prisma);
const licenseKeyService = await LicenseKeyService.create();
const hasValidLicense = await licenseKeyService.checkLicense();

let upId = token.upId;

Expand Down
6 changes: 4 additions & 2 deletions packages/features/auth/lib/next-auth-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import CredentialsProvider from "next-auth/providers/credentials";
import EmailProvider from "next-auth/providers/email";
import GoogleProvider from "next-auth/providers/google";

import checkLicense from "@calcom/features/ee/common/server/checkLicense";
import LicenseKeyService from "@calcom/ee/common/server/LicenseKeyService";
import createUsersAndConnectToOrg from "@calcom/features/ee/dsync/lib/users/createUsersAndConnectToOrg";
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
import { getOrgFullOrigin, subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
Expand Down Expand Up @@ -619,7 +619,9 @@ export const AUTH_OPTIONS: AuthOptions = {
},
async session({ session, token, user }) {
log.debug("callbacks:session - Session callback called", safeStringify({ session, token, user }));
const hasValidLicense = await checkLicense(prisma);
const licenseKeyService = await LicenseKeyService.create();
const hasValidLicense = await licenseKeyService.checkLicense();

const profileId = token.profileId;
const calendsoSession: Session = {
...session,
Expand Down
Loading

0 comments on commit e16662e

Please sign in to comment.