@coder/backstage-plugin-coder
Version:
Create and manage Coder workspaces from Backstage
378 lines (375 loc) • 13.5 kB
JavaScript
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
import { createContext, useContext, useEffect, useState, useCallback, useRef, useLayoutEffect } from 'react';
import { createPortal } from 'react-dom';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useApi } from '@backstage/core-plugin-api';
import { makeStyles } from '@material-ui/core';
import { useId } from '../../hooks/hookPolyfills.esm.js';
import { BackstageHttpError } from '../../api/errors.esm.js';
import { sharedAuthQueryKey, CODER_QUERY_KEY_PREFIX } from '../../api/queryOptions.esm.js';
import { coderClientWrapperApiRef } from '../../api/CoderClient.esm.js';
import { coderAuthApiRef } from '../../api/CoderAuthApi.esm.js';
import { CoderLogo } from '../CoderLogo/CoderLogo.esm.js';
import { CoderAuthFormDialog } from '../CoderAuthFormDialog/CoderAuthFormDialog.esm.js';
const BACKSTAGE_APP_ROOT_ID = "#root";
const FALLBACK_UI_OVERRIDE_CLASS_NAME = "backstage-root-override";
const AUTH_GRACE_PERIOD_TIMEOUT_MS = 6e3;
const AuthTrackingContext = createContext(null);
const AuthStateContext = createContext(null);
const validAuthStatuses = [
"authenticated",
"distrustedWithGracePeriod"
];
function useAuthState() {
const coderClient = useApi(coderClientWrapperApiRef);
const coderAuthApi = useApi(coderAuthApiRef);
const [authToken, setAuthToken] = useState("");
const [readonlyInitialAuthToken, setReadonlyInitialAuthToken] = useState("");
const [isInsideGracePeriod, setIsInsideGracePeriod] = useState(true);
useEffect(() => {
let isMounted = true;
const loadTokenFromSession = async () => {
try {
const accessToken = await coderAuthApi.getAccessToken("", {
optional: true
});
if (isMounted && accessToken) {
coderClient.setToken(accessToken);
setAuthToken(accessToken);
setReadonlyInitialAuthToken(accessToken);
}
} catch (error) {
}
};
loadTokenFromSession();
return () => {
isMounted = false;
};
}, [coderAuthApi, coderClient]);
const queryIsEnabled = authToken !== "";
const authValidityQuery = useQuery({
queryKey: [...sharedAuthQueryKey, authToken],
queryFn: () => coderClient.syncToken(authToken),
enabled: queryIsEnabled,
keepPreviousData: queryIsEnabled,
// Can't use !query.state.data because we want to refetch on undefined cases
refetchOnWindowFocus: (query) => query.state.data !== false
});
const authState = generateAuthState({
authToken,
authValidityQuery,
isInsideGracePeriod,
initialAuthToken: readonlyInitialAuthToken
});
if (!isInsideGracePeriod && authState.status === "authenticated") {
setIsInsideGracePeriod(true);
}
useEffect(() => {
if (authState.status !== "distrustedWithGracePeriod") {
return void 0;
}
const distrustTimeoutId = window.setTimeout(() => {
setIsInsideGracePeriod(false);
}, AUTH_GRACE_PERIOD_TIMEOUT_MS);
return () => window.clearTimeout(distrustTimeoutId);
}, [authState.status]);
const isAuthenticated = validAuthStatuses.includes(authState.status);
const queryClient = useQueryClient();
useEffect(() => {
if (!isAuthenticated) {
return void 0;
}
let isRevalidating = false;
const revalidateTokenOnError = async (event) => {
const queryError = event.query.state.error;
const shouldRevalidate = isAuthenticated && !isRevalidating && BackstageHttpError.isInstance(queryError) && queryError.status === 401;
if (!shouldRevalidate) {
return;
}
isRevalidating = true;
await queryClient.refetchQueries({ queryKey: sharedAuthQueryKey });
isRevalidating = false;
};
const queryCache = queryClient.getQueryCache();
const unsubscribe = queryCache.subscribe(revalidateTokenOnError);
return unsubscribe;
}, [queryClient, isAuthenticated]);
const registerNewToken = useCallback((newToken) => {
if (newToken !== "") {
setAuthToken(newToken);
}
}, []);
const unlinkToken = useCallback(() => {
setAuthToken("");
coderAuthApi.signOut().catch(() => {
});
queryClient.removeQueries({ queryKey: [CODER_QUERY_KEY_PREFIX] });
}, [queryClient, coderAuthApi]);
return {
...authState,
isAuthenticated,
registerNewToken,
unlinkToken
};
}
function useAuthFallbackState() {
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
const [hasTrackers, setHasTrackers] = useState(false);
const trackedComponentsRef = useRef(null);
if (trackedComponentsRef.current === null) {
trackedComponentsRef.current = /* @__PURE__ */ new Set();
}
const trackComponent = useCallback((componentId) => {
const syncTrackerToUi = () => {
setHasTrackers(trackedComponentsRef.current.size > 0);
};
trackedComponentsRef.current.add(componentId);
syncTrackerToUi();
return () => {
trackedComponentsRef.current.delete(componentId);
syncTrackerToUi();
};
}, []);
return {
trackComponent,
hasNoAuthInputs: isMounted && !hasTrackers
};
}
function useInternalCoderAuth() {
const trackComponent = useContext(AuthTrackingContext);
if (trackComponent === null) {
throw new Error("Unable to retrieve state for displaying fallback auth UI");
}
const instanceId = useId();
useEffect(() => {
const cleanupTracking = trackComponent(instanceId);
return cleanupTracking;
}, [instanceId, trackComponent]);
return useEndUserCoderAuth();
}
function useEndUserCoderAuth() {
const authContextValue = useContext(AuthStateContext);
if (authContextValue === null) {
throw new Error("Cannot retrieve auth information from CoderProvider");
}
return authContextValue;
}
function generateAuthState({
authToken,
initialAuthToken,
authValidityQuery,
isInsideGracePeriod
}) {
const isInitializing = initialAuthToken !== "" && authValidityQuery.isLoading && authValidityQuery.isFetching && !authValidityQuery.isFetchedAfterMount;
if (isInitializing) {
return {
status: "initializing",
token: void 0,
error: void 0
};
}
if (!authToken) {
return {
status: "tokenMissing",
token: void 0,
error: void 0
};
}
if (BackstageHttpError.isInstance(authValidityQuery.error)) {
const deploymentLikelyUnavailable = authValidityQuery.error.status === 504 || authValidityQuery.error.status === 200 && authValidityQuery.error.contentType !== "application/json";
if (deploymentLikelyUnavailable) {
return {
status: "deploymentUnavailable",
token: void 0,
error: authValidityQuery.error
};
}
}
const isTokenValidFromPrevFetch = authValidityQuery.data === true;
if (isTokenValidFromPrevFetch) {
const canTrustAuthThisRender = authValidityQuery.isSuccess && !authValidityQuery.isPaused;
if (canTrustAuthThisRender) {
return {
status: "authenticated",
token: authToken,
error: void 0
};
}
if (isInsideGracePeriod) {
return {
status: "distrustedWithGracePeriod",
token: authToken,
error: void 0
};
}
return {
status: "distrusted",
token: void 0,
error: authValidityQuery.error
};
}
const isAuthenticating = authValidityQuery.isLoading || authValidityQuery.isRefetching && (authValidityQuery.isError && authValidityQuery.data !== true || authValidityQuery.isSuccess && authValidityQuery.data === false);
if (isAuthenticating) {
return {
status: "authenticating",
token: void 0,
error: authValidityQuery.error
};
}
const isCoderDeploymentDown = authValidityQuery.error instanceof Error && authValidityQuery.error.name === "TimeoutError";
if (isCoderDeploymentDown) {
return {
status: "distrusted",
token: void 0,
error: authValidityQuery.error
};
}
if (authValidityQuery.isPaused) {
return {
status: "noInternetConnection",
token: void 0,
error: authValidityQuery.error
};
}
return {
status: "invalid",
token: void 0,
error: authValidityQuery.error
};
}
const mainAppRoot = document.querySelector(BACKSTAGE_APP_ROOT_ID);
const useFallbackStyles = makeStyles((theme) => ({
landmarkWrapper: ({ isDialogOpen }) => ({
zIndex: isDialogOpen ? 0 : 9999,
position: "fixed",
bottom: theme.spacing(2),
width: "100%",
maxWidth: "fit-content",
left: "50%",
transform: "translateX(-50%)"
}),
dialogButton: {
display: "flex",
flexFlow: "row nowrap",
columnGap: theme.spacing(1),
alignItems: "center"
},
logo: {
fill: theme.palette.primary.contrastText,
width: theme.spacing(3)
}
}));
function FallbackAuthUi() {
const fallbackRef = useRef(null);
useLayoutEffect(() => {
const fallback = fallbackRef.current;
const mainAppContainer = mainAppRoot?.querySelector("main") ?? null;
if (fallback === null || mainAppContainer === null) {
return void 0;
}
const overrideStyleNode = document.createElement("style");
overrideStyleNode.type = "text/css";
const liveAppStyles = getComputedStyle(mainAppContainer);
const liveFallbackStyles = getComputedStyle(fallback);
let prevPaddingBottom = void 0;
const updatePaddingForFallbackUi = () => {
const prevInnerHtml = overrideStyleNode.innerHTML;
overrideStyleNode.innerHTML = "";
const paddingBottomWithNoOverride = liveAppStyles.paddingBottom || "0px";
if (paddingBottomWithNoOverride === prevPaddingBottom) {
overrideStyleNode.innerHTML = prevInnerHtml;
return;
}
const fallbackBottom = parseInt(liveFallbackStyles.bottom || "0", 10);
const normalized = Number.isNaN(fallbackBottom) ? 0 : fallbackBottom;
const paddingToAdd = fallback.offsetHeight + normalized;
overrideStyleNode.innerHTML = `
.${FALLBACK_UI_OVERRIDE_CLASS_NAME} {
padding-bottom: calc(${paddingBottomWithNoOverride} + ${paddingToAdd}px) !important;
}
`;
prevPaddingBottom = paddingBottomWithNoOverride;
};
const observer = new MutationObserver(updatePaddingForFallbackUi);
observer.observe(document.head, { childList: true });
observer.observe(mainAppContainer, {
childList: false,
subtree: false,
attributes: true,
attributeFilter: ["class", "style"]
});
document.head.append(overrideStyleNode);
mainAppContainer.classList.add(FALLBACK_UI_OVERRIDE_CLASS_NAME);
return () => {
observer.disconnect();
overrideStyleNode.remove();
mainAppContainer.classList.remove(FALLBACK_UI_OVERRIDE_CLASS_NAME);
};
}, []);
const hookId = useId();
const [isDialogOpen, setIsDialogOpen] = useState(false);
const styles = useFallbackStyles({ isDialogOpen });
const landmarkId = `${hookId}-landmark`;
const fallbackUi = /* @__PURE__ */ jsxs(
"section",
{
ref: fallbackRef,
className: styles.landmarkWrapper,
"aria-labelledby": landmarkId,
children: [
/* @__PURE__ */ jsx("h2", { id: landmarkId, hidden: true, children: "Authenticate with Coder to enable Coder plugin features" }),
/* @__PURE__ */ jsxs(
CoderAuthFormDialog,
{
open: isDialogOpen,
onOpen: () => setIsDialogOpen(true),
onClose: () => setIsDialogOpen(false),
triggerClassName: styles.dialogButton,
children: [
/* @__PURE__ */ jsx(CoderLogo, { className: styles.logo }),
"Authenticate with Coder"
]
}
)
]
}
);
return createPortal(fallbackUi, document.body);
}
const dummyTrackComponent = () => {
return () => {
};
};
const fallbackProviders = {
hidden: ({ children }) => /* @__PURE__ */ jsx(AuthTrackingContext.Provider, { value: dummyTrackComponent, children }),
assertive: ({ children, isAuthenticated }) => (
// Don't need the live version of the tracker function if we're always
// going to be showing the fallback auth input no matter what
/* @__PURE__ */ jsxs(AuthTrackingContext.Provider, { value: dummyTrackComponent, children: [
children,
!isAuthenticated && /* @__PURE__ */ jsx(FallbackAuthUi, {})
] })
),
// Have to give function a name to satisfy ES Lint (rules of hooks)
restrained: function Restrained({ children, isAuthenticated }) {
const { hasNoAuthInputs, trackComponent } = useAuthFallbackState();
const needFallbackUi = !isAuthenticated && hasNoAuthInputs;
return /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx(AuthTrackingContext.Provider, { value: trackComponent, children }),
needFallbackUi && /* @__PURE__ */ jsx(AuthTrackingContext.Provider, { value: dummyTrackComponent, children: /* @__PURE__ */ jsx(FallbackAuthUi, {}) })
] });
}
};
function CoderAuthProvider({
children,
fallbackAuthUiMode = "restrained"
}) {
const authState = useAuthState();
const AuthFallbackProvider = fallbackProviders[fallbackAuthUiMode];
return /* @__PURE__ */ jsx(AuthStateContext.Provider, { value: authState, children: /* @__PURE__ */ jsx(AuthFallbackProvider, { isAuthenticated: authState.isAuthenticated, children }) });
}
export { AuthStateContext, AuthTrackingContext, CoderAuthProvider, dummyTrackComponent, useEndUserCoderAuth, useInternalCoderAuth };
//# sourceMappingURL=CoderAuthProvider.esm.js.map