feat: removes account dropdown from oauth consent screen (#27191)

* remove acount dropdown on consent screen

* change button back to go back

* add missing translations

* fix avatar

* code clean up

* add translation

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
This commit is contained in:
Carina WollendorferandCarinaWolli authored and GitHub committed 2026-01-28 15:34:59 +01:00
1 parent ade823f135
commit cba329d6d2
3 files changed
+159 -66

No files matched your search

+130 -62
View File
@@ -9,14 +9,17 @@ import { APP_NAME } from "@calcom/lib/constants";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import { Avatar } from "@calcom/ui/components/avatar";
import { Button } from "@calcom/ui/components/button";
import { Select } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import { Tooltip } from "@calcom/ui/components/tooltip";
export function Authorize() {
const { t } = useLocale();
const { status } = useSession();
const { data: user } = useMeQuery();
const router = useRouter();
const searchParams = useCompatSearchParams();
@@ -26,11 +29,18 @@ export function Authorize() {
const state = searchParams?.get("state") as string;
const scope = searchParams?.get("scope") as string;
const code_challenge = searchParams?.get("code_challenge") as string;
const code_challenge_method = searchParams?.get("code_challenge_method") as string;
const code_challenge_method = searchParams?.get(
"code_challenge_method"
) as string;
const show_account_selector =
searchParams?.get("show_account_selector") === "true";
const queryString = searchParams?.toString();
const [selectedAccount, setSelectedAccount] = useState<{ value: string; label: string } | null>();
const [selectedAccount, setSelectedAccount] = useState<{
value: string;
label: string;
} | null>();
const scopes = scope ? scope.toString().split(",") : [];
const {
@@ -48,23 +58,30 @@ export function Authorize() {
);
const { data, isPending: isPendingProfiles } =
trpc.viewer.loggedInViewerRouter.teamsAndUserProfilesQuery.useQuery();
const generateAuthCodeMutation = trpc.viewer.oAuth.generateAuthCode.useMutation({
onSuccess: (data) => {
window.location.href =
data.redirectUrl ?? `${client?.redirectUri}?code=${data.authorizationCode}&state=${state}`;
},
onError: (error) => {
if (client?.redirectUri) {
redirectToOAuthError({
redirectUri: client.redirectUri,
trpcError: error,
state,
});
trpc.viewer.loggedInViewerRouter.teamsAndUserProfilesQuery.useQuery(
undefined,
{
enabled: show_account_selector,
}
},
});
);
const generateAuthCodeMutation =
trpc.viewer.oAuth.generateAuthCode.useMutation({
onSuccess: (data) => {
window.location.href =
data.redirectUrl ??
`${client?.redirectUri}?code=${data.authorizationCode}&state=${state}`;
},
onError: (error) => {
if (client?.redirectUri) {
redirectToOAuthError({
redirectUri: client.redirectUri,
trpcError: error,
state,
});
}
},
});
const mappedProfiles = data
? data
@@ -76,14 +93,14 @@ export function Authorize() {
: [];
useEffect(() => {
if (mappedProfiles.length > 0) {
if (show_account_selector && mappedProfiles.length > 0) {
setSelectedAccount(mappedProfiles[0]);
}
}, [isPendingProfiles]);
}, [isPendingProfiles, show_account_selector]);
// Auto-authorize trusted clients
useEffect(() => {
if (client?.isTrusted && selectedAccount) {
if (client?.isTrusted) {
generateAuthCodeMutation.mutate({
clientId: client_id as string,
redirectUri: client.redirectUri,
@@ -93,7 +110,7 @@ export function Authorize() {
state,
});
}
}, [client?.isTrusted, selectedAccount]);
}, [client?.isTrusted]);
useEffect(() => {
if (status === "unauthenticated") {
@@ -113,7 +130,11 @@ export function Authorize() {
return <div>{getClientError.message}</div>;
}
if (isPendingGetClient || isPendingProfiles || status !== "authenticated") {
if (
isPendingGetClient ||
(show_account_selector && isPendingProfiles) ||
status !== "authenticated"
) {
return <></>;
}
@@ -122,75 +143,113 @@ export function Authorize() {
}
// Don't show UI for trusted clients, they'll auto-authorize
if (client.isTrusted && selectedAccount) {
if (client.isTrusted) {
return (
<div className="flex justify-center pt-32">
<div className="flex items-center space-x-3">
<span className="text-lg font-medium text-gray-700">Authorizing...</span>
<span className="text-lg font-medium text-gray-700">
{t("authorizing")}
</span>
</div>
</div>
);
}
return (
<div className="flex min-h-screen items-center justify-center">
<div className="bg-default border-subtle mt-2 max-w-xl rounded-md border px-9 pt-2 pb-3">
<div className="flex items-center justify-center">
<div className="flex justify-center items-center min-h-screen">
<div className="px-9 pt-2 pb-3 mt-2 max-w-xl rounded-md border bg-default border-subtle">
<div className="flex justify-center items-center">
<Avatar
alt=""
fallback={<Icon name="plus" className="text-subtle h-6 w-6" />}
fallback={<Icon name="plus" className="w-6 h-6 text-subtle" />}
className="items-center"
imageSrc={client.logo}
size="lg"
/>
<div className="relative -ml-6 h-24 w-24">
<div className="absolute inset-0 flex items-center justify-center">
<div className="relative -ml-6 w-24 h-24">
<div className="flex absolute inset-0 justify-center items-center">
<div className="bg-default flex h-[70px] w-[70px] items-center justify-center rounded-full">
<img src="/cal-com-icon.svg" alt="Logo" className="h-16 w-16 rounded-full" />
<img
src="/cal-com-icon.svg"
alt="Logo"
className="w-16 h-16 rounded-full"
/>
</div>
</div>
</div>
</div>
<h1 className="px-5 pt-3 pb-5 text-center text-2xl font-bold tracking-tight">
{t("access_cal_account", { clientName: client.name, appName: APP_NAME })}
<h1 className="px-5 pt-3 pb-3 text-2xl font-semibold tracking-tight text-center">
{t("access_cal_account", {
clientName: client.name,
appName: APP_NAME,
})}
</h1>
<div className="mb-1 text-sm font-medium">{t("select_account_team")}</div>
<Select
isSearchable={true}
id="account-select"
onChange={(value) => {
setSelectedAccount(value);
}}
className="w-52"
defaultValue={selectedAccount || mappedProfiles[0]}
options={mappedProfiles}
/>
<div className="mt-5 mb-4 font-medium">{t("allow_client_to", { clientName: client.name })}</div>
<ul className="stack-y-4 text-sm">
{!show_account_selector && (
<div className="flex flex-col justify-center items-center mb-6 text-sm text-gray-600">
<div className="flex gap-2 items-center" data-testid="signed-in-user">
<Avatar
size="sm"
alt={user?.username ? t("avatar_of_username", { username: user.username }) : t("avatar")}
imageSrc={user?.avatarUrl ?? user?.avatar}
/>
<Tooltip content={user?.email} side="bottom">
<span className="cursor-default">
{t("signed_in_as", { name: user?.name || user?.email })}
</span>
</Tooltip>
</div>
</div>
)}
{show_account_selector && (
<>
<div className="mb-1 text-sm font-medium">
{t("select_account_team")}
</div>
<Select
isSearchable={true}
id="account-select"
onChange={(value) => {
setSelectedAccount(value);
}}
className="w-52"
defaultValue={selectedAccount || mappedProfiles[0]}
options={mappedProfiles}
/>
</>
)}
<div className="mt-8 mb-4 text-sm font-semibold">
{t("allow_client_to", { clientName: client.name })}
</div>
<ul className="text-sm stack-y-3">
<li className="relative pl-5">
<span className="absolute left-0">&#10003;</span>{" "}
{t("associate_with_cal_account", { clientName: client.name })}
</li>
<li className="relative pl-5">
<span className="absolute left-0">&#10003;</span> {t("see_personal_info")}
<span className="absolute left-0">&#10003;</span>
{t("see_personal_info")}
</li>
<li className="relative pl-5">
<span className="absolute left-0">&#10003;</span> {t("see_primary_email_address")}
<span className="absolute left-0">&#10003;</span>
{t("see_primary_email_address")}
</li>
<li className="relative pl-5">
<span className="absolute left-0">&#10003;</span> {t("connect_installed_apps")}
<span className="absolute left-0">&#10003;</span>
</li>
<li className="relative pl-5">
<span className="absolute left-0">&#10003;</span> {t("access_event_type")}
<span className="absolute left-0">&#10003;</span>
{t("access_event_type")}
</li>
<li className="relative pl-5">
<span className="absolute left-0">&#10003;</span> {t("access_availability")}
<span className="absolute left-0">&#10003;</span>
{t("access_availability")}
</li>
<li className="relative pl-5">
<span className="absolute left-0">&#10003;</span> {t("access_bookings")}
<span className="absolute left-0">&#10003;</span>
{t("access_bookings")}
</li>
</ul>
<div className="bg-subtle mt-8 mb-8 flex rounded-md p-3">
<div className="flex p-3 mt-8 mb-8 rounded-md bg-subtle">
<div>
<Icon name="info" className="mr-1 mt-0.5 h-4 w-4" />
</div>
@@ -198,10 +257,12 @@ export function Authorize() {
<div className="mb-1 text-sm font-medium">
{t("allow_client_to_do", { clientName: client.name })}
</div>
<div className="text-sm">{t("oauth_access_information", { appName: APP_NAME })}</div>{" "}
<div className="text-sm">
{t("oauth_access_information", { appName: APP_NAME })}
</div>{" "}
</div>
</div>
<div className="border-subtle border- -mx-9 mb-4 border-b" />
<div className="-mx-9 mb-4 border-b border-subtle border-" />
<div className="flex justify-end">
<Button
className="mr-2"
@@ -213,8 +274,11 @@ export function Authorize() {
if (state) {
params.set("state", state);
}
window.location.href = `${client.redirectUri}${separator}${params.toString()}`;
}}>
window.location.href = `${
client.redirectUri
}${separator}${params.toString()}`;
}}
>
{t("go_back")}
</Button>
<Button
@@ -227,11 +291,13 @@ export function Authorize() {
? selectedAccount?.value.substring(5)
: undefined, // team account starts with /team/<slug>
codeChallenge: code_challenge || undefined,
codeChallengeMethod: (code_challenge_method as "S256") || undefined,
codeChallengeMethod:
(code_challenge_method as "S256") || undefined,
state,
});
}}
data-testid="allow-button">
data-testid="allow-button"
>
{t("allow")}
</Button>
</div>
@@ -269,7 +335,9 @@ function buildOAuthErrorRedirectUrl({
errorParams.append("state", state);
}
return `${redirectUri}${redirectUri.includes("?") ? "&" : "?"}${errorParams.toString()}`;
return `${redirectUri}${
redirectUri.includes("?") ? "&" : "?"
}${errorParams.toString()}`;
}
function redirectToOAuthError({
@@ -291,4 +359,4 @@ function redirectToOAuthError({
window.location.href = redirectUrl;
}
export default Authorize;
export default Authorize;
+23 -2
View File
@@ -123,12 +123,33 @@ test.describe("OAuth Provider", () => {
expect(validTokenData.username.startsWith("test user")).toBe(true);
});
test("should show signed-in user name instead of account selector when show_account_selector is not set", async ({
page,
users,
}) => {
const user = await users.create({ username: "test user", name: "test user" });
await user.apiLogin();
await page.goto(
`auth/oauth2/authorize?client_id=${client.clientId}&redirect_uri=${client.redirectUri}&response_type=code&scope=READ_PROFILE&state=1234`
);
await expect(page).toHaveURL(/auth\/oauth2\/authorize/);
await page.waitForSelector('[data-testid="allow-button"]');
// Should show "Signed in as" with the user's name
await expect(page.getByTestId("signed-in-user")).toBeVisible();
// Account selector should not be present
await expect(page.locator("#account-select")).not.toBeVisible();
});
test("should create valid access token & refresh token for team", async ({ page, users }) => {
const user = await users.create({ username: "test user", name: "test user" }, { hasTeam: true });
await user.apiLogin();
await page.goto(
`auth/oauth2/authorize?client_id=${client.clientId}&redirect_uri=${client.redirectUri}&response_type=code&scope=READ_PROFILE&state=1234`
`auth/oauth2/authorize?client_id=${client.clientId}&redirect_uri=${client.redirectUri}&response_type=code&scope=READ_PROFILE&state=1234&show_account_selector=true`
);
await page.locator("#account-select").click();
@@ -227,7 +248,7 @@ test.describe("OAuth Provider", () => {
await page.locator("#password").fill(user.username || "");
await page.locator('[type="submit"]').click();
await page.waitForSelector("#account-select");
await page.waitForSelector('[data-testid="allow-button"]');
await expect(page.getByText("test user")).toBeVisible();
});
@@ -1024,6 +1024,8 @@
"hours": "Hours",
"people": "People",
"your_email": "Your email",
"avatar": "Avatar",
"avatar_of_username": "Avatar of {{username}}",
"change_avatar": "Change avatar",
"upload_avatar": "Upload avatar",
"language": "Language",
@@ -2904,7 +2906,7 @@
"event_setup_length_error": "Event Setup: The duration must be at least 1 minute.",
"availability_schedules": "Availability schedules",
"unauthorized": "Unauthorized",
"access_cal_account": "{{clientName}} would like access to your {{appName}} account",
"access_cal_account": "{{clientName}} wants to access your {{appName}} account",
"select_account_team": "Select account or team",
"allow_client_to": "This will allow {{clientName}} to",
"associate_with_cal_account": "Associate you with your personal info from {{clientName}}",
@@ -2915,8 +2917,10 @@
"access_availability": "Read, edit, delete your availability",
"access_bookings": "Read, edit, delete your bookings",
"allow_client_to_do": "Allow {{clientName}} to do this?",
"oauth_access_information": "By clicking allow, you allow this app to use your information in accordance with their terms of service and privacy policy. You can remove access in the {{appName}} App Store.",
"oauth_access_information": "By clicking allow, you allow this app to use your information in accordance with their terms of service and privacy policy.",
"allow": "Allow",
"authorizing": "Authorizing...",
"signed_in_as": "Signed in as {{name}}",
"view_only_edit_availability_not_onboarded": "This user has not completed onboarding. You will not be able to set their availability until they have completed onboarding.",
"view_only_edit_availability": "You are viewing this user's availability. You can only edit your own availability.",
"you_can_override_calendar_in_advanced_tab": "You can override this on a per-event basis in the advanced settings in each event type.",