import React, { useEffect, useMemo, useState } from "react"; import { Alert, Anchor, Box, Button, Center, Checkbox, Container, Divider, Group, Loader, Paper, Stack, Text, ThemeIcon, Title, } from "@mantine/core"; import { notifications } from "@mantine/notifications"; import { IconAlertTriangle, IconEye, IconPencil, } from "@tabler/icons-react"; import { useQuery } from "@tanstack/react-query"; import { useAtom } from "jotai"; import { RESET } from "jotai/utils"; import { useTranslation } from "react-i18next"; import { useLocation, useSearchParams } from "react-router-dom"; import { AuthLayout } from "@/features/auth/components/auth-layout.tsx"; import classes from "@/features/auth/components/auth.module.css"; import { DocumentTitle } from "@/components/ui/document-title.tsx"; import { UserInfo } from "@/components/common/user-info.tsx"; import useCurrentUser from "@/features/user/hooks/use-current-user"; import { currentUserAtom } from "@/features/user/atoms/current-user-atom"; import { logout } from "@/features/auth/services/auth-service"; import { ICurrentUser } from "@/features/user/types/user.types"; import APP_ROUTE from "@/lib/app-route.ts"; import { approveOAuthAuthorization, getOAuthAuthorizeInfo, } from "@/ee/oauth/services/oauth-service"; import { IApproveAuthorizationPayload, IAuthorizeParams, IOAuthAuthorizeInfo, } from "@/ee/oauth/types/oauth.types"; function loginRedirectUrl(pathname: string, search: string): string { return `${APP_ROUTE.AUTH.LOGIN}?redirect=${encodeURIComponent(pathname + search)}`; } function errorStatus(error: any): number | undefined { return error?.response?.status; } function errorText(error: any): string | undefined { const data = error?.response?.data; if (typeof data?.error_description === "string") return data.error_description; if (Array.isArray(data?.message)) return data.message.join(", "); return data?.message; } const OAUTH_PARAM_NAMES = [ "response_type", "client_id", "redirect_uri", "state", "code_challenge", "code_challenge_method", "scope", "resource", ]; function pickOAuthParams(params: IAuthorizeParams): IAuthorizeParams { const picked: IAuthorizeParams = {}; for (const name of OAUTH_PARAM_NAMES) { if (params[name] !== undefined) { picked[name] = params[name]; } } return picked; } export default function OAuthConsent() { const { t } = useTranslation(); const location = useLocation(); const [searchParams] = useSearchParams(); const params = useMemo( () => Object.fromEntries(searchParams.entries()), [searchParams], ); const currentUserQuery = useCurrentUser(); const infoQuery = useQuery({ queryKey: ["oauth-authorize-info", params], queryFn: () => getOAuthAuthorizeInfo(params), }); const isUnauthenticated = errorStatus(currentUserQuery.error) === 401 || errorStatus(infoQuery.error) === 401; useEffect(() => { if (isUnauthenticated) { window.location.replace( loginRedirectUrl(location.pathname, location.search), ); } }, [isUnauthenticated, location.pathname, location.search]); const isLoading = isUnauthenticated || currentUserQuery.isLoading || infoQuery.isLoading; return ( {isLoading ? (
) : infoQuery.data && currentUserQuery.data ? ( ) : ( )}
); } function InvalidRequestCard({ description }: { description?: string }) { const { t } = useTranslation(); return ( {t("Invalid authorization request")} {description && ( {description} )} ); } type ConsentCardProps = { info: IOAuthAuthorizeInfo; currentUser: ICurrentUser; params: IAuthorizeParams; }; function ConsentCard({ info, currentUser, params }: ConsentCardProps) { const { t } = useTranslation(); const location = useLocation(); const [, setCurrentUser] = useAtom(currentUserAtom); const [approvedScopes, setApprovedScopes] = useState( info.scopes.filter((scope) => scope === "read" || scope === "write"), ); const [submitting, setSubmitting] = useState<"approve" | "deny" | null>(null); const scopeRows = [ { scope: "read", icon: , label: t("Read"), description: t("View data in your workspace without making changes."), }, { scope: "write", icon: , label: t("Write"), description: t("Create and modify data in your workspace."), }, ]; function toggleScope(scope: string, checked: boolean) { setApprovedScopes((prev) => checked ? [...prev, scope] : prev.filter((item) => item !== scope), ); } async function submitDecision(approved: boolean) { setSubmitting(approved ? "approve" : "deny"); const oauthParams = pickOAuthParams(params); const payload: IApproveAuthorizationPayload = approved ? { ...oauthParams, approved: true, approvedScopes } : { ...oauthParams, approved: false }; try { const res = await approveOAuthAuthorization(payload); window.location.replace(res.redirectUrl); } catch (err) { if (errorStatus(err) === 401) { window.location.replace( loginRedirectUrl(location.pathname, location.search), ); return; } setSubmitting(null); notifications.show({ message: errorText(err) || t("Something went wrong. Please try again."), color: "red", }); } } async function switchAccount() { if (submitting !== null) { return; } setCurrentUser(RESET); try { await logout(); } finally { window.location.replace( loginRedirectUrl(location.pathname, location.search), ); } } return ( {t("{{name}} wants to access {{workspace}}", { name: info.clientName, workspace: currentUser.workspace.name, })} {t("Not you? Switch account")} {t("This application will be able to:")} {scopeRows.map((row) => { const requested = info.scopes.includes(row.scope); return ( toggleScope(row.scope, event.currentTarget.checked) } label={ {row.icon} {row.label} } description={row.description} /> ); })}
{t("You will be redirected to")} {info.redirectUri}
{!info.verified && ( } > {t("Make sure you trust this application before authorizing it.")} )}
); }