Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
5dc87b3
feat: add OAuth client developer settings page with approval workflow
devin-ai-integration[bot] Dec 2, 2025
c6c68d4
fix: re-export generateSecret for backward compatibility
devin-ai-integration[bot] Dec 2, 2025
a3f1bc9
feat: make logo mandatory and list items clickable for OAuth clients
devin-ai-integration[bot] Dec 8, 2025
b1b39a2
fix: add missing translation keys and remove client secret from detai…
devin-ai-integration[bot] Dec 8, 2025
03c7d13
fix: address cubic AI reviewer comments
devin-ai-integration[bot] Dec 8, 2025
582cef5
fix: address PR review comments - fix indentation and use useCopy hook
devin-ai-integration[bot] Dec 8, 2025
c3e0b70
fix: change react-dom/server import to fix Turbopack compatibility
devin-ai-integration[bot] Dec 9, 2025
c29dbb3
Revert "fix: change react-dom/server import to fix Turbopack compatib…
devin-ai-integration[bot] Dec 9, 2025
c5f13a5
fix: use email service pattern for OAuth client notifications
devin-ai-integration[bot] Dec 9, 2025
fc9d47c
fix: add try-catch around email sending to handle Turbopack react-dom…
devin-ai-integration[bot] Dec 9, 2025
3804fe3
Revert "fix: add try-catch around email sending to handle Turbopack r…
devin-ai-integration[bot] Dec 9, 2025
05172df
fix: improve OAuth client UI with skeleton loaders and smaller dialog…
devin-ai-integration[bot] Dec 9, 2025
dfd5bc2
fix: improve skeleton loader to match actual OAuth client list structure
devin-ai-integration[bot] Dec 9, 2025
7500de8
fix skeleton
eunjae-lee Dec 9, 2025
a1d86ac
rename the selected oauth client dialog
eunjae-lee Dec 9, 2025
bc6f449
fix: address PR feedback - admin auth, dropdown styling, sidebar label
devin-ai-integration[bot] Dec 9, 2025
523db2f
update common.json
eunjae-lee Dec 9, 2025
661ffa7
feat: show client secret in approval email for confidential OAuth cli…
devin-ai-integration[bot] Dec 14, 2025
5b0b738
feat: add Website URL field, fix logo styling, show client secret aft…
devin-ai-integration[bot] Dec 22, 2025
a05ffea
fix: move clientSecret variable declaration outside if block for prop…
devin-ai-integration[bot] Dec 22, 2025
3fedda6
Merge branch 'main' into devin/oauth-developer-settings-1764693294
supalarry Jan 7, 2026
80dc2ed
refactor: dont expose client secret in emails
supalarry Jan 7, 2026
50daba7
Merge branch 'main' into devin/oauth-developer-settings-1764693294
supalarry Jan 8, 2026
098ebcc
refactor: dont regenerate secret upon status change
supalarry Jan 8, 2026
c0a7c5f
refactor: reuse existing hash function
supalarry Jan 8, 2026
c384a37
refactor: rename admin/oAuth to admin/oauth page
supalarry Jan 8, 2026
82274aa
Merge branch 'main' into devin/oauth-developer-settings-1764693294
supalarry Jan 8, 2026
93c2fd7
refactor: deduplicate oauth repositories
supalarry Jan 8, 2026
8415ef0
refactor: remove withGlobalPrisma from oauth repository
supalarry Jan 8, 2026
681f7a9
Merge branch 'main' into devin/oauth-developer-settings-1764693294
supalarry Jan 8, 2026
fa7458a
refactor: developer oauth page
supalarry Jan 8, 2026
d163a1d
refactor: oauth status by default accepted
supalarry Jan 8, 2026
cf5c4e3
refactor: request oauth status when creating
supalarry Jan 8, 2026
bb0af5b
refactor ux
supalarry Jan 9, 2026
74b8174
fix: address Cubic AI code review feedback
devin-ai-integration[bot] Jan 9, 2026
8652dbd
Merge branch 'main' into devin/oauth-developer-settings-1764693294
supalarry Jan 12, 2026
f6b3852
common.json file
supalarry Jan 12, 2026
daf04c4
refactor: delete all prisma migrations
supalarry Jan 12, 2026
8ee1ee5
refactor: have just 1 prisma migration
supalarry Jan 12, 2026
f4cdd32
revert: some devin changes
supalarry Jan 12, 2026
9b43e0a
fix: typecheck
supalarry Jan 12, 2026
db38f71
test: owner OAuth crud
supalarry Jan 12, 2026
a8d1688
test: admin OAuth approval / rejection
supalarry Jan 12, 2026
953e930
Merge branch 'main' into devin/oauth-developer-settings-1764693294
supalarry Jan 12, 2026
44dbc67
fix: address Cubic AI review feedback (confidence 9/10 issues)
devin-ai-integration[bot] Jan 12, 2026
fe44f1f
cubic changes
Jan 13, 2026
ddaa111
refactor: dont log sensitive info and rethrow error
supalarry Jan 13, 2026
91f1f1c
cubic feedback
supalarry Jan 13, 2026
60fd4db
refactor: make oauth client purpose optional
supalarry Jan 13, 2026
6691d50
refactor: admin/oauth not allowed if not logged in
supalarry Jan 13, 2026
1a7453e
refactor: admin view skeleton
supalarry Jan 13, 2026
2795286
refactor: rename state
supalarry Jan 13, 2026
32d61ba
refactor: get rid of redundant mapping
supalarry Jan 13, 2026
d8ba093
refactor: remove redundant handler
supalarry Jan 13, 2026
14d5d92
refactor: remove redundant handler
supalarry Jan 13, 2026
bc146ab
refactor: re-usable new oauth client button
supalarry Jan 13, 2026
e9e0018
refactor: dialogs
supalarry Jan 13, 2026
100e0a1
refactor: modals
supalarry Jan 14, 2026
44a0f67
refactor: handler names, dialog, skeleton
supalarry Jan 14, 2026
350799d
fix: purpose being null
supalarry Jan 14, 2026
44ce7bd
refactor: rename handler and delete old oauth admin page
supalarry Jan 14, 2026
bb0faa8
Merge branch 'main' into devin/oauth-developer-settings-1764693294
supalarry Jan 14, 2026
c958f96
fix: purpose in submission
supalarry Jan 14, 2026
54ec8b9
refactor: handler names
supalarry Jan 14, 2026
1c6f039
refactor: rename
supalarry Jan 14, 2026
b6ebb15
refactor: update handler
supalarry Jan 14, 2026
52dc384
refactor: rename approvalStatus -> status
supalarry Jan 14, 2026
47723a8
refactor: simplify modal
supalarry Jan 14, 2026
df18f3c
refactor: name
supalarry Jan 14, 2026
ec19534
dont require repproval if redirectUri changes
supalarry Jan 14, 2026
1aa80b4
Merge branch 'main' into devin/oauth-developer-settings-1764693294
supalarry Jan 14, 2026
9457c40
Merge branch 'main' into devin/oauth-developer-settings-1764693294
supalarry Jan 15, 2026
6cc78d4
fix: remove integration sync index creation
supalarry Jan 15, 2026
a5c38be
refactor: require re-approval if redirectUri updated
supalarry Jan 15, 2026
154eee1
fix: flaky e2e test
supalarry Jan 15, 2026
83eccde
fix: flaky e2e test
supalarry Jan 15, 2026
b2888f4
fix: flaky e2e test
supalarry Jan 16, 2026
0b03d23
Merge branch 'main' into devin/oauth-developer-settings-1764693294
supalarry Jan 16, 2026
ce0fb75
fix: remove duplicate common.json keys
supalarry Jan 16, 2026
4a80d67
refactor: replace team@cal.com with SUPPORT_MAIL_ADDRESS
supalarry Jan 16, 2026
1e5854b
refactor: generate client secret on handler level
supalarry Jan 16, 2026
e8b809d
fix: authorization code only available to approved clients
supalarry Jan 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";

import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";

import LegacyPage from "~/settings/admin/oauth-view";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";

import OAuthClientsAdminView from "~/settings/admin/oauth-clients-admin-view";

const Page = async () => {
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
await getTranslate();

if (!session) {
redirect("/auth/login?callbackUrl=/settings/admin/oauth");
}

return <OAuthClientsAdminView />;
};

export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("oAuth"),
(t) => t("admin_oAuth_description"),
(t) => t("oauth_clients_admin"),
(t) => t("oauth_clients_admin_description"),
undefined,
undefined,
"/settings/admin/oAuth"
"/settings/admin/oauth"
);

const Page = async () => {
const t = await getTranslate();
return (
<SettingsHeader title={t("oAuth")} description={t("admin_oAuth_description")}>
<LegacyPage />
</SettingsHeader>
);
};

export default Page;
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ const getTabs = (orgBranding: OrganizationBranding | null) => {
href: "/settings/developer/api-keys",
trackingMetadata: { section: "developer", page: "api_keys" },
},
{
name: "oAuth",
href: "/settings/developer/oauth",
trackingMetadata: { section: "developer", page: "oauth_clients" }
},
{
name: "admin_api",
href: "/settings/organizations/admin-api",
Expand Down Expand Up @@ -280,7 +285,7 @@ const getTabs = (orgBranding: OrganizationBranding | null) => {
},
{
name: "oAuth",
href: "/settings/admin/oAuth",
href: "/settings/admin/oauth",
trackingMetadata: { section: "admin", page: "oauth" },
},
{
Expand All @@ -297,7 +302,7 @@ const getTabs = (orgBranding: OrganizationBranding | null) => {
},
];

tabs.find((tab) => {
for (const tab of tabs) {
if (tab.name === "security" && !HOSTED_CAL_FEATURES) {
tab.children?.push({
name: "sso_configuration",
Expand All @@ -307,21 +312,21 @@ const getTabs = (orgBranding: OrganizationBranding | null) => {
// TODO: Enable dsync for self hosters
// tab.children?.push({ name: "directory_sync", href: "/settings/security/dsync" });
}

if (tab.name === "admin" && IS_CALCOM) {
tab.children?.push({
name: "create_org",
href: "/settings/organizations/new",
trackingMetadata: { section: "admin", page: "create_org" },
});
}
if (tab.name === "admin" && IS_CALCOM) {

tab.children?.push({
name: "create_license_key",
href: "/settings/license-key/new",
trackingMetadata: { section: "admin", page: "create_license_key" },
});
}
});
}

return tabs;
};
Expand All @@ -337,7 +342,7 @@ const organizationAdminKeys = [
"delegation_credential",
];

export interface SettingsPermissions {
interface SettingsPermissions {
canViewRoles?: boolean;
canViewOrganizationBilling?: boolean;
canUpdateOrganization?: boolean;
Expand Down Expand Up @@ -553,7 +558,7 @@ const TeamListCollapsible = ({ teamFeatures }: { teamFeatures?: Record<number, T
if (!teamMenuState[index]) {
return null;
}
if (teamMenuState.some((teamState) => teamState.teamId === team.id))
if (teamMenuState.some((teamState) => teamState.teamId === team.id)) {
return (
<Collapsible
className="cursor-pointer"
Expand Down Expand Up @@ -696,6 +701,9 @@ const TeamListCollapsible = ({ teamFeatures }: { teamFeatures?: Record<number, T
</CollapsibleContent>
</Collapsible>
);
}

return null;
})}
</>
);
Expand Down Expand Up @@ -1033,14 +1041,14 @@ const MobileSettingsContainer = (props: { onSideContainerOpen?: () => void }) =>
);
};

export type SettingsLayoutProps = {
type SettingsLayoutProps = {
children: React.ReactNode;
containerClassName?: string;
teamFeatures?: Record<number, TeamFeatures>;
permissions?: SettingsPermissions;
} & ComponentProps<typeof Shell>;

export default function SettingsLayoutAppDirClient({
function SettingsLayoutAppDirClient({
children,
teamFeatures,
permissions,
Expand Down Expand Up @@ -1129,3 +1137,6 @@ const SidebarContainerElement = ({
</>
);
};

export type { SettingsLayoutProps, SettingsPermissions };
export default SettingsLayoutAppDirClient;
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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 { buildLegacyRequest } from "@lib/buildLegacyCtx";

import OAuthClientsView from "~/settings/developer/oauth-clients-view";

export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("oauth_clients"),
(t) => t("oauth_clients_description"),
undefined,
undefined,
"/settings/developer/oauth"
);

const Page = async () => {
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
const t = await getTranslate();

if (!session) {
redirect("/auth/login?callbackUrl=/settings/developer/oauth");
}

return (
<OAuthClientsView />
);
};

export default Page;
4 changes: 4 additions & 0 deletions apps/web/app/api/auth/oauth/token/__tests__/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ describe("POST /api/auth/oauth/token", () => {
redirectUri: "https://app.example.com/callback",
clientSecret: null,
clientType: "PUBLIC" as const,
status: "APPROVED" as const,
} as const;

const mockAccessCode = {
Expand Down Expand Up @@ -279,6 +280,7 @@ describe("POST /api/auth/oauth/token", () => {
redirectUri: "https://app.example.com/callback",
clientSecret: "hashed_secret",
clientType: "CONFIDENTIAL" as const,
status: "APPROVED" as const,
} as const;

const mockAccessCode = {
Expand Down Expand Up @@ -494,6 +496,7 @@ describe("POST /api/auth/oauth/token", () => {
redirectUri: "https://app.example.com/callback",
clientSecret: null,
clientType: "PUBLIC" as const,
status: "APPROVED" as const,
} as Awaited<ReturnType<typeof prismaMock.oAuthClient.findUnique>>);

const tokenRequest = createTokenRequest({
Expand All @@ -517,6 +520,7 @@ describe("POST /api/auth/oauth/token", () => {
redirectUri: "https://app.example.com/callback",
clientSecret: null,
clientType: "PUBLIC" as const,
status: "APPROVED" as const,
} as Awaited<ReturnType<typeof prismaMock.oAuthClient.findUnique>>);
prismaMock.accessCode.findFirst.mockResolvedValue(null);

Expand Down
42 changes: 42 additions & 0 deletions apps/web/modules/settings/admin/oauth-clients-admin-skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"use client";

import { SkeletonText, SkeletonContainer } from "@calcom/ui/components/skeleton";

export const OAuthClientsAdminSkeleton = () => {
return (
<SkeletonContainer>
<div className="mb-8">
<SkeletonText className="h-7 w-64" />
<div className="mt-2 flex items-start justify-between gap-4">
<SkeletonText className="h-4 w-full max-w-xl" />
<div className="bg-emphasis h-9 w-20 rounded-md" />
</div>
</div>

<div className="space-y-10">
{[1, 2, 3].map((section) => (
<div key={section} className="space-y-3">
<SkeletonText className="h-5 w-24" />
<div className="border-subtle rounded-lg border">
{[1, 2, 3].map((row) => (
<div
// eslint-disable-next-line react/no-array-index-key
key={`${section}-${row}`}
className={`flex items-center justify-between p-4 ${row !== 3 ? "border-subtle border-b" : ""}`}>
<div className="flex items-center gap-4">
<div className="bg-emphasis h-10 w-10 rounded-full" />
<SkeletonText className="h-4 w-40" />
</div>
<div className="flex items-center gap-4">
<SkeletonText className="h-5 w-20 rounded-full" />
<div className="bg-emphasis h-5 w-5 rounded" />
</div>
</div>
))}
</div>
</div>
))}
</div>
</SkeletonContainer>
);
};
Loading