diff --git a/apps/web/modules/auth/oauth2/authorize-view.tsx b/apps/web/modules/auth/oauth2/authorize-view.tsx index 3dd41fe5c8..c66ba5d3c4 100644 --- a/apps/web/modules/auth/oauth2/authorize-view.tsx +++ b/apps/web/modules/auth/oauth2/authorize-view.tsx @@ -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
{getClientError.message}
; } - 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 (
- Authorizing... + + {t("authorizing")} +
); } return ( -
-
-
+
+
+
} + fallback={} className="items-center" imageSrc={client.logo} size="lg" /> -
-
+
+
- Logo + Logo
-

- {t("access_cal_account", { clientName: client.name, appName: APP_NAME })} +

+ {t("access_cal_account", { + clientName: client.name, + appName: APP_NAME, + })}

-
{t("select_account_team")}
- { + setSelectedAccount(value); + }} + className="w-52" + defaultValue={selectedAccount || mappedProfiles[0]} + options={mappedProfiles} + /> + + )} +
+ {t("allow_client_to", { clientName: client.name })} +
+
  • ✓{" "} {t("associate_with_cal_account", { clientName: client.name })}
  • - ✓ {t("see_personal_info")} + ✓ + {t("see_personal_info")}
  • - ✓ {t("see_primary_email_address")} + ✓ + {t("see_primary_email_address")}
  • - ✓ {t("connect_installed_apps")} + ✓
  • - ✓ {t("access_event_type")} + ✓ + {t("access_event_type")}
  • - ✓ {t("access_availability")} + ✓ + {t("access_availability")}
  • - ✓ {t("access_bookings")} + ✓ + {t("access_bookings")}
-
+
@@ -198,10 +257,12 @@ export function Authorize() {
{t("allow_client_to_do", { clientName: client.name })}
-
{t("oauth_access_information", { appName: APP_NAME })}
{" "} +
+ {t("oauth_access_information", { appName: APP_NAME })} +
{" "}
-
+
@@ -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; \ No newline at end of file +export default Authorize; diff --git a/apps/web/playwright/oauth-provider.e2e.ts b/apps/web/playwright/oauth-provider.e2e.ts index 1f700a9bc3..4e39e5196e 100644 --- a/apps/web/playwright/oauth-provider.e2e.ts +++ b/apps/web/playwright/oauth-provider.e2e.ts @@ -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(); }); diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 9a0e2dc522..23ec816bb1 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -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.",