From a379bd2b52e48e635edd913000c25940a18dc8fa Mon Sep 17 00:00:00 2001
From: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Date: Tue, 9 Sep 2025 14:27:00 +0100
Subject: [PATCH] fix: org layout authentication (#23707)
* refactor layout to not check session
* add actions for orgs + org admins
* update types on actions to be correctly non nullable
* Add tests for utils
* Apply suggestion from @coderabbitai[bot]
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* restore lock file
* rename _actions to actions
* update tests to be mocked
* restore lock
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
---
.../attributes/[id]/edit/page.tsx | 4 +
.../attributes/create/page.tsx | 4 +
.../(org-admin-only)/attributes/page.tsx | 10 +-
.../(org-admin-only)/billing/page.tsx | 36 +-
.../delegation-credential/page.tsx | 4 +
.../(org-admin-only)/dsync/page.tsx | 11 +-
.../(org-admin-only)/privacy/page.tsx | 8 +-
.../(org-admin-only)/sso/page.tsx | 6 +-
.../actions/validateUserHasOrg.test.ts | 229 ++++++++++
.../actions/validateUserHasOrg.tsx | 30 ++
.../actions/validateUserHasOrgAdmin.test.ts | 418 ++++++++++++++++++
.../actions/validateUserHasOrgAdmin.tsx | 45 ++
.../organizations/admin-api/page.tsx | 4 +
.../organizations/general/page.tsx | 11 +-
.../organizations/layout.tsx | 15 -
.../organizations/profile/page.tsx | 14 +-
.../organizations/roles/page.tsx | 7 +-
.../teams/other/(main-page)/page.tsx | 6 +-
.../teams/other/[id]/appearance/page.tsx | 4 +
.../teams/other/[id]/members/page.tsx | 4 +
.../teams/other/[id]/profile/page.tsx | 4 +
21 files changed, 804 insertions(+), 70 deletions(-)
create mode 100644 apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/actions/validateUserHasOrg.test.ts
create mode 100644 apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/actions/validateUserHasOrg.tsx
create mode 100644 apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/actions/validateUserHasOrgAdmin.test.ts
create mode 100644 apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/actions/validateUserHasOrgAdmin.tsx
diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/attributes/[id]/edit/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/attributes/[id]/edit/page.tsx
index b4562aebe0..c60161bb78 100644
--- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/attributes/[id]/edit/page.tsx
+++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/attributes/[id]/edit/page.tsx
@@ -2,6 +2,8 @@ import { _generateMetadata } from "app/_utils";
import OrgAttributesEditPage from "@calcom/ee/organizations/pages/settings/attributes/attributes-edit-view";
+import { validateUserHasOrgAdmin } from "../../../../actions/validateUserHasOrgAdmin";
+
export const generateMetadata = async ({ params }: { params: Promise<{ id: string }> }) =>
await _generateMetadata(
(t) => t("attribute"),
@@ -12,6 +14,8 @@ export const generateMetadata = async ({ params }: { params: Promise<{ id: strin
);
const OrgAttributesEditPageWrapper = async () => {
+ await validateUserHasOrgAdmin();
+
return ;
};
diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/attributes/create/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/attributes/create/page.tsx
index 4040958f0d..6a8b0690b3 100644
--- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/attributes/create/page.tsx
+++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/attributes/create/page.tsx
@@ -2,6 +2,8 @@ import { _generateMetadata } from "app/_utils";
import OrgAttributesCreatePage from "@calcom/ee/organizations/pages/settings/attributes/attributes-create-view";
+import { validateUserHasOrgAdmin } from "../../../actions/validateUserHasOrgAdmin";
+
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("attribute"),
@@ -12,6 +14,8 @@ export const generateMetadata = async () =>
);
const OrgAttributesCreatePageWrapper = async () => {
+ await validateUserHasOrgAdmin();
+
return ;
};
diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/attributes/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/attributes/page.tsx
index cfa0900043..343fbc45e1 100644
--- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/attributes/page.tsx
+++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/attributes/page.tsx
@@ -1,15 +1,13 @@
import { _generateMetadata, getTranslate } from "app/_utils";
-import { headers, cookies } from "next/headers";
import { redirect } from "next/navigation";
import OrgSettingsAttributesPage from "@calcom/ee/organizations/pages/settings/attributes/attributes-list-view";
-import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { MembershipRole } from "@calcom/prisma/enums";
-import { buildLegacyRequest } from "@lib/buildLegacyCtx";
+import { validateUserHasOrgAdmin } from "../../actions/validateUserHasOrgAdmin";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -22,11 +20,7 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
- const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
-
- if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) {
- return redirect("/settings/profile");
- }
+ const session = await validateUserHasOrgAdmin();
const { canRead, canEdit, canDelete, canCreate } = await getResourcePermissions({
userId: session.user.id,
diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/billing/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/billing/page.tsx
index 268371cb2d..4b4f2b3930 100644
--- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/billing/page.tsx
+++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/billing/page.tsx
@@ -1,5 +1,35 @@
-import BillingPage, { generateMetadata } from "../../../billing/page";
+import { _generateMetadata } from "app/_utils";
+import { getTranslate } from "app/_utils";
-export { generateMetadata };
+import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
-export default BillingPage;
+import BillingView from "~/settings/billing/billing-view";
+
+import { validateUserHasOrgAdmin } from "../../actions/validateUserHasOrgAdmin";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("billing"),
+ (t) => t("manage_billing_description"),
+ undefined,
+ undefined,
+ "/settings/organizations/billing"
+ );
+
+const Page = async () => {
+ const t = await getTranslate();
+ await validateUserHasOrgAdmin();
+
+ // TODO(SEAN): Add PBAC to this page in the next PR
+
+ return (
+
+
+
+ );
+};
+
+export default Page;
diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/delegation-credential/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/delegation-credential/page.tsx
index 3faca06291..20e5d6af4f 100644
--- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/delegation-credential/page.tsx
+++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/delegation-credential/page.tsx
@@ -3,6 +3,8 @@ import { _generateMetadata, getTranslate } from "app/_utils";
import DelegationCredentialList from "@calcom/features/ee/organizations/pages/settings/delegationCredential";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
+import { validateUserHasOrgAdmin } from "../../actions/validateUserHasOrgAdmin";
+
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("delegation_credential"),
@@ -15,6 +17,8 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
+ await validateUserHasOrgAdmin();
+
return (
await _generateMetadata(
@@ -22,11 +19,7 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
- const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
-
- if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) {
- return redirect("/settings/organizations/general");
- }
+ const session = await validateUserHasOrgAdmin();
const { canEdit } = await getResourcePermissions({
userId: session.user.id,
diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/privacy/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/privacy/page.tsx
index 5423446d54..1d786ea76f 100644
--- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/privacy/page.tsx
+++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/privacy/page.tsx
@@ -1,15 +1,13 @@
import { _generateMetadata, getTranslate } from "app/_utils";
-import { headers, cookies } from "next/headers";
import { redirect } from "next/navigation";
-import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import PrivacyView from "@calcom/features/ee/organizations/pages/settings/privacy";
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { MembershipRole } from "@calcom/prisma/enums";
-import { buildLegacyRequest } from "@lib/buildLegacyCtx";
+import { validateUserHasOrgAdmin } from "../../actions/validateUserHasOrgAdmin";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -21,9 +19,9 @@ export const generateMetadata = async () =>
);
const Page = async () => {
- const [t, _headers, _cookies] = await Promise.all([getTranslate(), headers(), cookies()]);
+ const t = await getTranslate();
- const session = await getServerSession({ req: buildLegacyRequest(_headers, _cookies) });
+ const session = await validateUserHasOrgAdmin();
if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) {
return redirect("/settings/profile");
diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/sso/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/sso/page.tsx
index a3fcf936b1..12b8c76905 100644
--- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/sso/page.tsx
+++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/sso/page.tsx
@@ -1,15 +1,13 @@
import { _generateMetadata, getTranslate } from "app/_utils";
-import { headers, cookies } from "next/headers";
import { redirect } from "next/navigation";
-import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import OrgSSOView from "@calcom/features/ee/sso/page/orgs-sso-view";
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { MembershipRole } from "@calcom/prisma/enums";
-import { buildLegacyRequest } from "@lib/buildLegacyCtx";
+import { validateUserHasOrgAdmin } from "../../actions/validateUserHasOrgAdmin";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -22,7 +20,7 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
- const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
+ const session = await validateUserHasOrgAdmin();
if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) {
return redirect("/settings/organizations/general");
diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/actions/validateUserHasOrg.test.ts b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/actions/validateUserHasOrg.test.ts
new file mode 100644
index 0000000000..ecc2f9eece
--- /dev/null
+++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/actions/validateUserHasOrg.test.ts
@@ -0,0 +1,229 @@
+import type { Session } from "next-auth";
+import { redirect } from "next/navigation";
+import { describe, it, vi, expect, beforeEach, type MockedFunction } from "vitest";
+
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
+import { MembershipRole } from "@calcom/prisma/enums";
+
+import { validateUserHasOrg, type ValidatedOrgSession } from "./validateUserHasOrg";
+
+// Mock the dependencies
+vi.mock("next/navigation", () => ({
+ redirect: vi.fn(),
+}));
+
+vi.mock("next/headers", () => ({
+ cookies: vi.fn(() => Promise.resolve({})),
+ headers: vi.fn(() => Promise.resolve({})),
+}));
+
+vi.mock("@calcom/features/auth/lib/getServerSession", () => ({
+ getServerSession: vi.fn(),
+}));
+
+vi.mock("@lib/buildLegacyCtx", () => ({
+ buildLegacyRequest: vi.fn(() => ({})),
+}));
+
+const mockedGetServerSession = getServerSession as MockedFunction;
+const mockedRedirect = vi.mocked(redirect);
+
+describe("validateUserHasOrg", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ const createMockSession = (overrides: Partial = {}): Session => ({
+ expires: "2025-01-01",
+ hasValidLicense: true,
+ user: {
+ id: 123,
+ email: "test@example.com",
+ name: "Test User",
+ org: {
+ id: 456,
+ name: "Test Org",
+ slug: "test-org",
+ logoUrl: null,
+ fullDomain: "test-org.cal.com",
+ domainSuffix: "cal.com",
+ role: MembershipRole.ADMIN,
+ },
+ profile: {
+ id: 789,
+ upId: "usr-123",
+ username: "testuser",
+ organizationId: 456,
+ organization: {
+ id: 456,
+ name: "Test Org",
+ slug: "test-org",
+ calVideoLogo: null,
+ bannerUrl: null,
+ requestedSlug: null,
+ },
+ },
+ },
+ profileId: 789,
+ upId: "usr-123",
+ ...overrides,
+ });
+
+ describe("when user has valid organization", () => {
+ it("should return session when user has org in user.org", async () => {
+ const mockSession = createMockSession();
+ mockedGetServerSession.mockResolvedValue(mockSession);
+
+ const result = await validateUserHasOrg();
+
+ expect(result).toEqual(mockSession);
+ expect(mockedRedirect).not.toHaveBeenCalled();
+ expect(result.user.id).toBe(123);
+ expect(result.user.org?.id).toBe(456);
+ expect(result.user.profile?.organizationId).toBe(456);
+ });
+
+ it("should return session when user has organizationId but no org", async () => {
+ const mockSession = createMockSession({
+ user: {
+ id: 123,
+ email: "test@example.com",
+ name: "Test User",
+ org: undefined,
+ profile: {
+ id: 789,
+ upId: "usr-123",
+ username: "testuser",
+ organizationId: 456,
+ organization: {
+ id: 456,
+ name: "Test Org",
+ slug: "test-org",
+ calVideoLogo: null,
+ bannerUrl: null,
+ requestedSlug: null,
+ },
+ },
+ },
+ });
+ mockedGetServerSession.mockResolvedValue(mockSession);
+
+ const result = await validateUserHasOrg();
+
+ expect(result).toEqual(mockSession);
+ expect(mockedRedirect).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("when user does not have valid organization", () => {
+ it("should redirect when session is null", async () => {
+ mockedGetServerSession.mockResolvedValue(null);
+
+ const redirectError = new Error("NEXT_REDIRECT");
+ mockedRedirect.mockImplementation(() => {
+ throw redirectError;
+ });
+
+ await expect(validateUserHasOrg()).rejects.toThrow("NEXT_REDIRECT");
+ expect(mockedRedirect).toHaveBeenCalledWith("/settings/my-account/profile");
+ });
+
+ it("should redirect when user has no organization data", async () => {
+ const mockSession = createMockSession({
+ user: {
+ id: 123,
+ email: "test@example.com",
+ name: "Test User",
+ org: undefined,
+ profile: {
+ id: 789,
+ upId: "usr-123",
+ username: "testuser",
+ organizationId: null,
+ organization: null,
+ },
+ },
+ });
+ mockedGetServerSession.mockResolvedValue(mockSession);
+
+ const redirectError = new Error("NEXT_REDIRECT");
+ mockedRedirect.mockImplementation(() => {
+ throw redirectError;
+ });
+
+ await expect(validateUserHasOrg()).rejects.toThrow("NEXT_REDIRECT");
+ expect(mockedRedirect).toHaveBeenCalledWith("/settings/my-account/profile");
+ });
+
+ it("should redirect when user has no user id", async () => {
+ const mockSession = createMockSession({
+ user: {
+ id: undefined as unknown as number,
+ email: "test@example.com",
+ name: "Test User",
+ org: {
+ id: 456,
+ name: "Test Org",
+ slug: "test-org",
+ logoUrl: null,
+ fullDomain: "test-org.cal.com",
+ domainSuffix: "cal.com",
+ role: MembershipRole.ADMIN,
+ },
+ profile: {
+ id: 789,
+ upId: "usr-123",
+ username: "testuser",
+ organizationId: 456,
+ organization: {
+ id: 456,
+ name: "Test Org",
+ slug: "test-org",
+ calVideoLogo: null,
+ bannerUrl: null,
+ requestedSlug: null,
+ },
+ },
+ },
+ });
+ mockedGetServerSession.mockResolvedValue(mockSession);
+
+ const redirectError = new Error("NEXT_REDIRECT");
+ mockedRedirect.mockImplementation(() => {
+ throw redirectError;
+ });
+
+ await expect(validateUserHasOrg()).rejects.toThrow("NEXT_REDIRECT");
+ expect(mockedRedirect).toHaveBeenCalledWith("/settings/my-account/profile");
+ });
+ });
+
+ describe("return type validation", () => {
+ it("should return ValidatedOrgSession type with non-nullable properties", async () => {
+ const mockSession = createMockSession();
+ mockedGetServerSession.mockResolvedValue(mockSession);
+
+ const result: ValidatedOrgSession = await validateUserHasOrg();
+
+ // Type assertions to ensure proper typing
+ expect(typeof result.user.id).toBe("number");
+ expect(result.user.org).toBeTruthy();
+ expect(result.user.profile).toBeTruthy();
+ expect(typeof result.user.profile.organizationId).toBe("number");
+
+ // Runtime checks that the types guarantee these properties exist
+ expect(result.user.id).toBe(123);
+ expect(result.user.org.id).toBe(456);
+ expect(result.user.profile.organizationId).toBe(456);
+ });
+ });
+
+ describe("error handling", () => {
+ it("should propagate errors from getServerSession", async () => {
+ const error = new Error("Session error");
+ mockedGetServerSession.mockRejectedValue(error);
+
+ await expect(validateUserHasOrg()).rejects.toThrow("Session error");
+ });
+ });
+});
diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/actions/validateUserHasOrg.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/actions/validateUserHasOrg.tsx
new file mode 100644
index 0000000000..a571e0b91b
--- /dev/null
+++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/actions/validateUserHasOrg.tsx
@@ -0,0 +1,30 @@
+import type { Session } from "next-auth";
+import { cookies, headers } from "next/headers";
+import { redirect } from "next/navigation";
+
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
+
+import { buildLegacyRequest } from "@lib/buildLegacyCtx";
+
+export type ValidatedOrgSession = NonNullable & {
+ user: NonNullable & {
+ id: number;
+ org: NonNullable["org"]>;
+ profile: NonNullable["profile"]> & {
+ organizationId: number;
+ };
+ };
+};
+
+export async function validateUserHasOrg(): Promise {
+ const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
+
+ const orgExists =
+ session?.user?.org || session?.user?.profile?.organizationId || session?.user?.profile?.organization;
+
+ if (!orgExists || !session?.user?.id || !session?.user?.profile?.organizationId) {
+ redirect("/settings/my-account/profile");
+ }
+
+ return session as ValidatedOrgSession;
+}
diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/actions/validateUserHasOrgAdmin.test.ts b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/actions/validateUserHasOrgAdmin.test.ts
new file mode 100644
index 0000000000..664fc02fac
--- /dev/null
+++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/actions/validateUserHasOrgAdmin.test.ts
@@ -0,0 +1,418 @@
+import type { Session } from "next-auth";
+import { redirect } from "next/navigation";
+import { describe, it, vi, expect, beforeEach, type MockedFunction } from "vitest";
+
+import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
+import { MembershipRole } from "@calcom/prisma/enums";
+
+import { validateUserHasOrgAdmin, type ValidatedOrgAdminSession } from "./validateUserHasOrgAdmin";
+
+// Mock the dependencies
+vi.mock("next/navigation", () => ({
+ redirect: vi.fn(),
+}));
+
+vi.mock("next/headers", () => ({
+ cookies: vi.fn(() => Promise.resolve({})),
+ headers: vi.fn(() => Promise.resolve({})),
+}));
+
+vi.mock("@calcom/features/auth/lib/getServerSession", () => ({
+ getServerSession: vi.fn(),
+}));
+
+vi.mock("@calcom/features/auth/lib/checkAdminOrOwner", () => ({
+ checkAdminOrOwner: vi.fn(),
+}));
+
+vi.mock("@lib/buildLegacyCtx", () => ({
+ buildLegacyRequest: vi.fn(() => ({})),
+}));
+
+const mockedGetServerSession = getServerSession as MockedFunction;
+const mockedCheckAdminOrOwner = vi.mocked(checkAdminOrOwner);
+const mockedRedirect = vi.mocked(redirect);
+
+describe("validateUserHasOrgAdmin", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ const createMockSession = (overrides: Partial = {}): Session => ({
+ expires: "2025-01-01",
+ hasValidLicense: true,
+ user: {
+ id: 123,
+ email: "test@example.com",
+ name: "Test User",
+ org: {
+ id: 456,
+ name: "Test Org",
+ slug: "test-org",
+ logoUrl: null,
+ fullDomain: "test-org.cal.com",
+ domainSuffix: "cal.com",
+ role: MembershipRole.ADMIN,
+ },
+ profile: {
+ id: 789,
+ upId: "usr-123",
+ username: "testuser",
+ organizationId: 456,
+ organization: {
+ id: 456,
+ name: "Test Org",
+ slug: "test-org",
+ calVideoLogo: null,
+ bannerUrl: null,
+ requestedSlug: null,
+ },
+ },
+ },
+ profileId: 789,
+ upId: "usr-123",
+ ...overrides,
+ });
+
+ describe("when user has valid organization and admin role", () => {
+ it("should return session when user is admin via user.org.role", async () => {
+ const mockSession = createMockSession();
+ mockedGetServerSession.mockResolvedValue(mockSession);
+ mockedCheckAdminOrOwner.mockReturnValue(true);
+
+ const result = await validateUserHasOrgAdmin();
+
+ expect(result).toEqual(mockSession);
+ expect(mockedRedirect).not.toHaveBeenCalled();
+ expect(mockedCheckAdminOrOwner).toHaveBeenCalledWith(MembershipRole.ADMIN);
+ expect(result.user.id).toBe(123);
+ expect(result.user.org?.id).toBe(456);
+ expect(result.user.profile?.organizationId).toBe(456);
+ });
+
+ it("should return session when user is owner", async () => {
+ const mockSession = createMockSession({
+ user: {
+ id: 123,
+ email: "test@example.com",
+ name: "Test User",
+ org: {
+ id: 456,
+ name: "Test Org",
+ slug: "test-org",
+ logoUrl: null,
+ fullDomain: "test-org.cal.com",
+ domainSuffix: "cal.com",
+ role: MembershipRole.OWNER,
+ },
+ profile: {
+ id: 789,
+ upId: "usr-123",
+ username: "testuser",
+ organizationId: 456,
+ organization: {
+ id: 456,
+ name: "Test Org",
+ slug: "test-org",
+ calVideoLogo: null,
+ bannerUrl: null,
+ requestedSlug: null,
+ },
+ },
+ },
+ });
+ mockedGetServerSession.mockResolvedValue(mockSession);
+ mockedCheckAdminOrOwner.mockReturnValue(true);
+
+ const result = await validateUserHasOrgAdmin();
+
+ expect(result).toEqual(mockSession);
+ expect(mockedRedirect).not.toHaveBeenCalled();
+ expect(mockedCheckAdminOrOwner).toHaveBeenCalledWith(MembershipRole.OWNER);
+ });
+
+ it("should return session when role comes from organization members", async () => {
+ const mockSession = createMockSession({
+ user: {
+ id: 123,
+ email: "test@example.com",
+ name: "Test User",
+ org: undefined,
+ profile: {
+ id: 789,
+ upId: "usr-123",
+ username: "testuser",
+ organizationId: 456,
+ organization: {
+ id: 456,
+ name: "Test Org",
+ slug: "test-org",
+ calVideoLogo: null,
+ bannerUrl: null,
+ requestedSlug: null,
+ members: [
+ { userId: 123, role: MembershipRole.ADMIN },
+ { userId: 124, role: MembershipRole.MEMBER },
+ ],
+ },
+ },
+ },
+ });
+ mockedGetServerSession.mockResolvedValue(mockSession);
+ mockedCheckAdminOrOwner.mockReturnValue(true);
+
+ const result = await validateUserHasOrgAdmin();
+
+ expect(result).toEqual(mockSession);
+ expect(mockedRedirect).not.toHaveBeenCalled();
+ expect(mockedCheckAdminOrOwner).toHaveBeenCalledWith(MembershipRole.ADMIN);
+ });
+ });
+
+ describe("when user does not have valid organization", () => {
+ it("should redirect when session is null", async () => {
+ mockedGetServerSession.mockResolvedValue(null);
+
+ const redirectError = new Error("NEXT_REDIRECT");
+ mockedRedirect.mockImplementation(() => {
+ throw redirectError;
+ });
+
+ await expect(validateUserHasOrgAdmin()).rejects.toThrow("NEXT_REDIRECT");
+ expect(mockedRedirect).toHaveBeenCalledWith("/settings/my-account/profile");
+ });
+
+ it("should redirect when user has no organization data", async () => {
+ const mockSession = createMockSession({
+ user: {
+ id: 123,
+ email: "test@example.com",
+ name: "Test User",
+ org: undefined,
+ profile: {
+ id: 789,
+ upId: "usr-123",
+ username: "testuser",
+ organizationId: null,
+ organization: null,
+ },
+ },
+ });
+ mockedGetServerSession.mockResolvedValue(mockSession);
+
+ const redirectError = new Error("NEXT_REDIRECT");
+ mockedRedirect.mockImplementation(() => {
+ throw redirectError;
+ });
+
+ await expect(validateUserHasOrgAdmin()).rejects.toThrow("NEXT_REDIRECT");
+ expect(mockedRedirect).toHaveBeenCalledWith("/settings/my-account/profile");
+ });
+ });
+
+ describe("when user has organization but is not admin", () => {
+ it("should redirect when user is only a member", async () => {
+ const mockSession = createMockSession({
+ user: {
+ id: 123,
+ email: "test@example.com",
+ name: "Test User",
+ org: {
+ id: 456,
+ name: "Test Org",
+ slug: "test-org",
+ logoUrl: null,
+ fullDomain: "test-org.cal.com",
+ domainSuffix: "cal.com",
+ role: MembershipRole.MEMBER,
+ },
+ profile: {
+ id: 789,
+ upId: "usr-123",
+ username: "testuser",
+ organizationId: 456,
+ organization: {
+ id: 456,
+ name: "Test Org",
+ slug: "test-org",
+ calVideoLogo: null,
+ bannerUrl: null,
+ requestedSlug: null,
+ },
+ },
+ },
+ });
+ mockedGetServerSession.mockResolvedValue(mockSession);
+ mockedCheckAdminOrOwner.mockReturnValue(false);
+
+ const redirectError = new Error("NEXT_REDIRECT");
+ mockedRedirect.mockImplementation(() => {
+ throw redirectError;
+ });
+
+ await expect(validateUserHasOrgAdmin()).rejects.toThrow("NEXT_REDIRECT");
+ expect(mockedRedirect).toHaveBeenCalledWith("/settings/organizations/profile");
+ expect(mockedCheckAdminOrOwner).toHaveBeenCalledWith(MembershipRole.MEMBER);
+ });
+
+ it("should redirect when role is undefined", async () => {
+ const mockSession = createMockSession({
+ user: {
+ id: 123,
+ email: "test@example.com",
+ name: "Test User",
+ org: undefined,
+ profile: {
+ id: 789,
+ upId: "usr-123",
+ username: "testuser",
+ organizationId: 456,
+ organization: {
+ id: 456,
+ name: "Test Org",
+ slug: "test-org",
+ calVideoLogo: null,
+ bannerUrl: null,
+ requestedSlug: null,
+ members: [
+ { userId: 999, role: MembershipRole.ADMIN }, // Different user
+ ],
+ },
+ },
+ },
+ });
+ mockedGetServerSession.mockResolvedValue(mockSession);
+ mockedCheckAdminOrOwner.mockReturnValue(false);
+
+ const redirectError = new Error("NEXT_REDIRECT");
+ mockedRedirect.mockImplementation(() => {
+ throw redirectError;
+ });
+
+ await expect(validateUserHasOrgAdmin()).rejects.toThrow("NEXT_REDIRECT");
+ expect(mockedRedirect).toHaveBeenCalledWith("/settings/organizations/profile");
+ expect(mockedCheckAdminOrOwner).toHaveBeenCalledWith(undefined);
+ });
+ });
+
+ describe("return type validation", () => {
+ it("should return ValidatedOrgAdminSession type with non-nullable properties", async () => {
+ const mockSession = createMockSession();
+ mockedGetServerSession.mockResolvedValue(mockSession);
+ mockedCheckAdminOrOwner.mockReturnValue(true);
+
+ const result: ValidatedOrgAdminSession = await validateUserHasOrgAdmin();
+
+ // Type assertions to ensure proper typing
+ expect(typeof result.user.id).toBe("number");
+ expect(result.user.org).toBeTruthy();
+ expect(result.user.profile).toBeTruthy();
+ expect(typeof result.user.profile.organizationId).toBe("number");
+ expect(result.user.org.role).toBeTruthy();
+
+ // Runtime checks that the types guarantee these properties exist
+ expect(result.user.id).toBe(123);
+ expect(result.user.org.id).toBe(456);
+ expect(result.user.org.role).toBe(MembershipRole.ADMIN);
+ expect(result.user.profile.organizationId).toBe(456);
+ });
+ });
+
+ describe("role resolution logic", () => {
+ it("should prefer org role over organization members role", async () => {
+ const mockSession = createMockSession({
+ user: {
+ id: 123,
+ email: "test@example.com",
+ name: "Test User",
+ org: {
+ id: 456,
+ name: "Test Org",
+ slug: "test-org",
+ logoUrl: null,
+ fullDomain: "test-org.cal.com",
+ domainSuffix: "cal.com",
+ role: MembershipRole.OWNER, // This should be preferred
+ },
+ profile: {
+ id: 789,
+ upId: "usr-123",
+ username: "testuser",
+ organizationId: 456,
+ organization: {
+ id: 456,
+ name: "Test Org",
+ slug: "test-org",
+ calVideoLogo: null,
+ bannerUrl: null,
+ requestedSlug: null,
+ members: [
+ { userId: 123, role: MembershipRole.MEMBER }, // Different role
+ ],
+ },
+ },
+ },
+ });
+ mockedGetServerSession.mockResolvedValue(mockSession);
+ mockedCheckAdminOrOwner.mockReturnValue(true);
+
+ await validateUserHasOrgAdmin();
+
+ expect(mockedCheckAdminOrOwner).toHaveBeenCalledWith(MembershipRole.OWNER);
+ });
+
+ it("should fall back to organization members role when org role is not available", async () => {
+ const mockSession = createMockSession({
+ user: {
+ id: 123,
+ email: "test@example.com",
+ name: "Test User",
+ org: undefined,
+ profile: {
+ id: 789,
+ upId: "usr-123",
+ username: "testuser",
+ organizationId: 456,
+ organization: {
+ id: 456,
+ name: "Test Org",
+ slug: "test-org",
+ calVideoLogo: null,
+ bannerUrl: null,
+ requestedSlug: null,
+ members: [{ userId: 123, role: MembershipRole.ADMIN }],
+ },
+ },
+ },
+ });
+ mockedGetServerSession.mockResolvedValue(mockSession);
+ mockedCheckAdminOrOwner.mockReturnValue(true);
+
+ await validateUserHasOrgAdmin();
+
+ expect(mockedCheckAdminOrOwner).toHaveBeenCalledWith(MembershipRole.ADMIN);
+ });
+ });
+
+ describe("error handling", () => {
+ it("should propagate errors from getServerSession", async () => {
+ const error = new Error("Session error");
+ mockedGetServerSession.mockRejectedValue(error);
+
+ await expect(validateUserHasOrgAdmin()).rejects.toThrow("Session error");
+ });
+
+ it("should propagate errors from checkAdminOrOwner", async () => {
+ const mockSession = createMockSession();
+ mockedGetServerSession.mockResolvedValue(mockSession);
+
+ const error = new Error("Permission check error");
+ mockedCheckAdminOrOwner.mockImplementation(() => {
+ throw error;
+ });
+
+ await expect(validateUserHasOrgAdmin()).rejects.toThrow("Permission check error");
+ });
+ });
+});
diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/actions/validateUserHasOrgAdmin.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/actions/validateUserHasOrgAdmin.tsx
new file mode 100644
index 0000000000..5491f4ef7b
--- /dev/null
+++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/actions/validateUserHasOrgAdmin.tsx
@@ -0,0 +1,45 @@
+import type { Session } from "next-auth";
+import { cookies, headers } from "next/headers";
+import { redirect } from "next/navigation";
+
+import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
+import type { MembershipRole } from "@calcom/prisma/enums";
+
+import { buildLegacyRequest } from "@lib/buildLegacyCtx";
+
+export type ValidatedOrgAdminSession = NonNullable & {
+ user: NonNullable & {
+ id: number;
+ org: NonNullable["org"]> & {
+ role: MembershipRole;
+ };
+ profile: NonNullable["profile"]> & {
+ organizationId: number;
+ };
+ };
+};
+
+export async function validateUserHasOrgAdmin(): Promise {
+ const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
+
+ const orgExists =
+ session?.user?.org || session?.user?.profile?.organizationId || session?.user?.profile?.organization;
+
+ if (!orgExists || !session?.user?.id || !session?.user?.profile?.organizationId) {
+ redirect("/settings/my-account/profile");
+ }
+
+ const userProfile = session?.user?.profile;
+ const userId = session?.user?.id;
+ const orgRole =
+ session?.user?.org?.role ??
+ userProfile?.organization?.members.find((m: { userId: number }) => m.userId === userId)?.role;
+ const isOrgAdminOrOwner = checkAdminOrOwner(orgRole);
+
+ if (!isOrgAdminOrOwner) {
+ redirect("/settings/organizations/profile");
+ }
+
+ return session as ValidatedOrgAdminSession;
+}
diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/admin-api/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/admin-api/page.tsx
index 4847949e49..d430d57432 100644
--- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/admin-api/page.tsx
+++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/admin-api/page.tsx
@@ -3,6 +3,8 @@ import { getTranslate, _generateMetadata } from "app/_utils";
import { AdminAPIView } from "@calcom/features/ee/organizations/pages/settings/admin-api";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
+import { validateUserHasOrg } from "../actions/validateUserHasOrg";
+
export const generateMetadata = async () =>
await _generateMetadata(
(t) => `${t("admin")} ${t("api_reference")}`,
@@ -15,6 +17,8 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
+ await validateUserHasOrg();
+
return (
await _generateMetadata(
@@ -23,11 +20,7 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
- const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
-
- if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) {
- return redirect("/settings/profile");
- }
+ const session = await validateUserHasOrg();
const { canRead, canEdit } = await getResourcePermissions({
userId: session.user.id,
diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/layout.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/layout.tsx
index fb858f6132..0ed221f7c7 100644
--- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/layout.tsx
+++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/layout.tsx
@@ -1,19 +1,4 @@
-import { cookies, headers } from "next/headers";
-import { redirect } from "next/navigation";
-
-import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
-
-import { buildLegacyRequest } from "@lib/buildLegacyCtx";
-
const SettingsOrganizationsLayout = async ({ children }: { children: React.ReactNode }) => {
- const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
-
- const orgExists =
- session?.user?.org || session?.user?.profile?.organizationId || session?.user?.profile?.organization;
- if (!orgExists) {
- return redirect("/settings/my-account/profile");
- }
-
return children;
};
diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx
index 4b58650b45..56a8914f99 100644
--- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx
+++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx
@@ -1,8 +1,6 @@
import { _generateMetadata, getTranslate } from "app/_utils";
-import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
-import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import LegacyPage from "@calcom/features/ee/organizations/pages/settings/profile";
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
@@ -10,7 +8,7 @@ import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import type { Membership } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
-import { buildLegacyRequest } from "@lib/buildLegacyCtx";
+import { validateUserHasOrg } from "../actions/validateUserHasOrg";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -22,20 +20,20 @@ export const generateMetadata = async () =>
);
const Page = async () => {
- const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
+ const session = await validateUserHasOrg();
const t = await getTranslate();
- const orgRole = session?.user.profile?.organization.members?.find(
- (member: Membership) => member.userId === session?.user.id
+ const orgRole = session.user.profile.organization.members?.find(
+ (member: Membership) => member.userId === session.user.id
)?.role;
- if (!session?.user.id || !session?.user.profile?.organizationId || !orgRole) {
+ if (!orgRole) {
return redirect("/settings/profile");
}
const { canRead, canEdit, canDelete } = await getResourcePermissions({
userId: session.user.id,
- teamId: session?.user.profile?.organizationId,
+ teamId: session.user.profile.organizationId,
resource: Resource.Organization,
userRole: orgRole,
fallbackRoles: {
diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/page.tsx
index 3b5466f46b..676a11bb02 100644
--- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/page.tsx
+++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/page.tsx
@@ -1,9 +1,7 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import { unstable_cache } from "next/cache";
-import { cookies, headers } from "next/headers";
import { notFound } from "next/navigation";
-import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import type { AppFlags } from "@calcom/features/flags/config";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { PermissionMapper } from "@calcom/features/pbac/domain/mappers/PermissionMapper";
@@ -13,8 +11,7 @@ import { RoleService } from "@calcom/features/pbac/services/role.service";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { prisma } from "@calcom/prisma";
-import { buildLegacyRequest } from "@lib/buildLegacyCtx";
-
+import { validateUserHasOrg } from "../actions/validateUserHasOrg";
import { CreateRoleCTA } from "./_components/CreateRoleCta";
import { RolesList } from "./_components/RolesList";
import { roleSearchParamsCache } from "./_components/searchParams";
@@ -58,7 +55,7 @@ export const generateMetadata = async () =>
const Page = async ({ searchParams }: { searchParams: Record }) => {
const t = await getTranslate();
- const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
+ const session = await validateUserHasOrg();
if (!session?.user?.org?.id || !session.user.id) {
return notFound();
diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/teams/other/(main-page)/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/teams/other/(main-page)/page.tsx
index f0e0154dd5..d8cee42e1e 100644
--- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/teams/other/(main-page)/page.tsx
+++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/teams/other/(main-page)/page.tsx
@@ -1,13 +1,11 @@
import { _generateMetadata, getTranslate } from "app/_utils";
-import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
-import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { OtherTeamsListing } from "@calcom/features/ee/organizations/pages/components/OtherTeamsListing";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { OrganizationRepository } from "@calcom/lib/server/repository/organization";
-import { buildLegacyRequest } from "@lib/buildLegacyCtx";
+import { validateUserHasOrg } from "../../../actions/validateUserHasOrg";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -20,7 +18,7 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
- const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
+ const session = await validateUserHasOrg();
if (!session?.user?.id) {
redirect("/auth/login");
diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/teams/other/[id]/appearance/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/teams/other/[id]/appearance/page.tsx
index f89432f785..e2a460cea6 100644
--- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/teams/other/[id]/appearance/page.tsx
+++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/teams/other/[id]/appearance/page.tsx
@@ -3,6 +3,8 @@ import { _generateMetadata, getTranslate } from "app/_utils";
import LegacyPage from "@calcom/features/ee/teams/pages/team-appearance-view";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
+import { validateUserHasOrg } from "../../../../actions/validateUserHasOrg";
+
export const generateMetadata = async ({ params }: { params: Promise<{ id: string }> }) =>
await _generateMetadata(
(t) => t("booking_appearance"),
@@ -15,6 +17,8 @@ export const generateMetadata = async ({ params }: { params: Promise<{ id: strin
const Page = async () => {
const t = await getTranslate();
+ await validateUserHasOrg();
+
return (
}) =>
await _generateMetadata(
(t) => t("team_members"),
@@ -17,6 +19,8 @@ export const generateMetadata = async ({ params }: { params: Promise<{ id: strin
const Page = async () => {
const t = await getTranslate();
+ await validateUserHasOrg();
+
return (
}) =>
await _generateMetadata(
(t) => t("profile"),
@@ -15,6 +17,8 @@ export const generateMetadata = async ({ params }: { params: Promise<{ id: strin
const Page = async () => {
const t = await getTranslate();
+ await validateUserHasOrg();
+
return (