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:
1 parent
ade823f135
commit
cba329d6d2
3 files changed
+159
-66
No files matched your search
@@ -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">✓</span>{" "}
|
||||
{t("associate_with_cal_account", { clientName: client.name })}
|
||||
</li>
|
||||
<li className="relative pl-5">
|
||||
<span className="absolute left-0">✓</span> {t("see_personal_info")}
|
||||
<span className="absolute left-0">✓</span>
|
||||
{t("see_personal_info")}
|
||||
</li>
|
||||
<li className="relative pl-5">
|
||||
<span className="absolute left-0">✓</span> {t("see_primary_email_address")}
|
||||
<span className="absolute left-0">✓</span>
|
||||
{t("see_primary_email_address")}
|
||||
</li>
|
||||
<li className="relative pl-5">
|
||||
<span className="absolute left-0">✓</span> {t("connect_installed_apps")}
|
||||
<span className="absolute left-0">✓</span>
|
||||
</li>
|
||||
<li className="relative pl-5">
|
||||
<span className="absolute left-0">✓</span> {t("access_event_type")}
|
||||
<span className="absolute left-0">✓</span>
|
||||
{t("access_event_type")}
|
||||
</li>
|
||||
<li className="relative pl-5">
|
||||
<span className="absolute left-0">✓</span> {t("access_availability")}
|
||||
<span className="absolute left-0">✓</span>
|
||||
{t("access_availability")}
|
||||
</li>
|
||||
<li className="relative pl-5">
|
||||
<span className="absolute left-0">✓</span> {t("access_bookings")}
|
||||
<span className="absolute left-0">✓</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;
|
||||
@@ -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.",
|
||||
|
||||
Reference in new issue
Block a user