UNPKG

terriajs

Version:

Geospatial data visualization platform.

441 lines (440 loc) 19.2 kB
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; import { useState, useEffect } from "react"; import { observer } from "mobx-react"; import URI from "urijs"; import { string } from "prop-types"; import { Trans, useTranslation } from "react-i18next"; import AddDataStyles from "./add-data.scss"; import Styles from "./cesium-ion-connector.scss"; import upsertModelFromJson from "../../../../Models/Definition/upsertModelFromJson"; import CatalogMemberFactory from "../../../../Models/Catalog/CatalogMemberFactory"; import CommonStrata from "../../../../Models/Definition/CommonStrata"; import addUserCatalogMember from "../../../../Models/Catalog/addUserCatalogMember"; import Dropdown from "../../../Generic/Dropdown"; import Icon from "../../../../Styled/Icon"; import classNames from "classnames"; import { RawButton } from "../../../../Styled/Button"; import styled from "styled-components"; import { useViewState } from "../../../Context"; import TimeVarying from "../../../../ModelMixins/TimeVarying"; import isDefined from "../../../../Core/isDefined"; const ActionButton = styled(RawButton) ` svg { height: 20px; width: 20px; margin: 5px; fill: ${(p) => p.theme.charcoalGrey}; } &:hover, &:focus { svg { fill: ${(p) => p.theme.modalHighlight}; } } `; class LoginTokenPersistenceInLocalStorage { storageName = "cesium-ion-login-token"; get() { return localStorage.getItem(this.storageName) ?? ""; } set(token) { localStorage.setItem(this.storageName, token); } clear() { localStorage.removeItem(this.storageName); } } class LoginTokenPersistenceInSessionStorage { storageName = "cesium-ion-login-token"; get() { return sessionStorage.getItem(this.storageName) ?? ""; } set(token) { sessionStorage.setItem(this.storageName, token); } clear() { sessionStorage.removeItem(this.storageName); } } class LoginTokenPersistenceInPage { loginToken = null; get() { return this.loginToken; } set(token) { this.loginToken = token; } clear() { this.loginToken = null; } } const loginTokenPersistenceTypes = { page: new LoginTokenPersistenceInPage(), sessionStorage: new LoginTokenPersistenceInSessionStorage(), localStorage: new LoginTokenPersistenceInLocalStorage() }; const loginTokenPersistenceLookup = loginTokenPersistenceTypes; const defaultUserProfile = { id: 0, scopes: [], username: "", email: "", emailVerified: false, avatar: string, storage: {} }; function CesiumIonConnector() { const viewState = useViewState(); const { t } = useTranslation(); const loginTokenPersistence = loginTokenPersistenceLookup[viewState.terria.configParameters.cesiumIonLoginTokenPersistence ?? ""] ?? loginTokenPersistenceTypes.page; const [codeChallenge, setCodeChallenge] = useState({ value: "", hash: "" }); const [tokens, setTokens] = useState([]); const [isLoadingTokens, setIsLoadingTokens] = useState(false); const [assets, setAssets] = useState([]); const [isLoadingAssets, setIsLoadingAssets] = useState(false); // This is the Cesium ion token representing the currently logged-in user, as obtained via // an OAuth2 Authorization Code Grant flow with Cesium ion. const [loginToken, setLoginToken] = useState(loginTokenPersistence.get() ?? ""); const [userProfile, setUserProfile] = useState(defaultUserProfile); const [isLoadingUserProfile, setIsLoadingUserProfile] = useState(false); useEffect(() => { if (!crypto || !crypto.subtle) return; const codeChallenge = [...crypto.getRandomValues(new Uint8Array(32))] .map((x) => x.toString(16).padStart(2, "0")) .join(""); crypto.subtle .digest("SHA-256", new TextEncoder().encode(codeChallenge)) .then((hash) => { setCodeChallenge({ value: codeChallenge, hash: btoa(String.fromCharCode(...new Uint8Array(hash))) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/[=]/g, "") }); }); }, []); useEffect(() => { if (loginToken.length === 0) return; setIsLoadingUserProfile(true); fetch("https://api.cesium.com/v1/me", { headers: { Authorization: `Bearer ${loginToken}` } }) .then((response) => { return response.json(); }) .then((profile) => { if (profile && profile.username) { setUserProfile(profile); } else { setUserProfile(defaultUserProfile); } setIsLoadingUserProfile(false); }); }, [loginToken]); useEffect(() => { if (loginToken.length === 0) return; setIsLoadingAssets(true); fetch("https://api.cesium.com/v1/assets", { headers: { Authorization: `Bearer ${loginToken}` } }) .then((response) => response.json()) .then((assets) => { if (assets.items) { assets.items.forEach((item) => { item.uniqueName = `${item.name} (${item.id})`; }); setAssets(assets.items); } setIsLoadingAssets(false); }); }, [loginToken]); useEffect(() => { if (!viewState.terria.configParameters.cesiumIonAllowSharingAddedAssets || loginToken.length === 0) { return; } setIsLoadingTokens(true); fetch("https://api.cesium.com/v2/tokens", { headers: { Authorization: `Bearer ${loginToken}` } }) .then((response) => response.json()) .then((tokens) => { if (tokens.items) { tokens.items.forEach((item) => { item.uniqueName = `${item.name} (${item.id})`; }); setTokens(tokens.items); } setIsLoadingTokens(false); }); }, [ loginToken, viewState.terria.configParameters.cesiumIonAllowSharingAddedAssets ]); let selectedToken = viewState.currentCesiumIonToken ? tokens.find((token) => token.id === viewState.currentCesiumIonToken) : undefined; if (selectedToken === undefined && tokens.length > 0) { selectedToken = tokens[0]; } const setSelectedToken = (token) => { viewState.currentCesiumIonToken = token.id; }; if (!crypto || !crypto.subtle) { return (_jsx("label", { className: AddDataStyles.label, children: "This service is not currently available. The most likely cause is that this web page is being accessed with `http` instead of `https`." })); } const dropdownTheme = { list: AddDataStyles.dropdownList, icon: _jsx(Icon, { glyph: Icon.GLYPHS.opened }), dropdown: Styles.dropDown, button: Styles.dropDownButton }; return (_jsxs(_Fragment, { children: [_jsx("label", { className: AddDataStyles.label, children: _jsx(Trans, { i18nKey: "addData.cesiumIon", children: _jsx("strong", { children: "Step 2:" }) }) }), loginToken.length > 0 ? renderConnectedOrConnecting() : renderDisconnected()] })); function renderConnectedOrConnecting() { return (_jsxs(_Fragment, { children: [isLoadingUserProfile ? (_jsx("label", { className: AddDataStyles.label, children: "Loading user profile information..." })) : (_jsxs("label", { className: AddDataStyles.label, children: ["Connected to Cesium ion as ", userProfile.username] })), _jsx("button", { className: Styles.connectButton, onClick: disconnect, children: "Disconnect" }), userProfile.username.length > 0 && renderConnected()] })); } function renderConnected() { const isAssetAccessibleBySelectedToken = (asset) => { if (!selectedToken) { if (viewState.terria.configParameters.cesiumIonAllowSharingAddedAssets) return false; else return true; } if (asset.id === undefined) return true; if (selectedToken.assetIds === undefined) { // Token allows access to all assets return true; } return selectedToken.assetIds.indexOf(asset.id) >= 0; }; return (_jsxs(_Fragment, { children: [renderTokenSelector(), isLoadingAssets ? (_jsx("label", { className: AddDataStyles.label, children: "Loading asset list..." })) : (_jsx("table", { className: Styles.assetsList, children: _jsxs("tbody", { children: [_jsxs("tr", { children: [_jsx("th", {}), _jsx("th", { children: "Name" }), _jsx("th", { children: "Type" })] }), assets .filter(isAssetAccessibleBySelectedToken) .map(renderAssetRow)] }) }))] })); } function renderTokenSelector() { if (!viewState.terria.configParameters.cesiumIonAllowSharingAddedAssets) return undefined; return (_jsxs(_Fragment, { children: [_jsx("label", { className: AddDataStyles.label, children: _jsx(Trans, { i18nKey: "addData.cesiumIonToken", children: "Cesium ion Token:" }) }), isLoadingTokens ? (_jsx("label", { className: AddDataStyles.label, children: "Loading token list..." })) : (_jsx(Dropdown, { options: tokens, textProperty: "uniqueName", selected: selectedToken, selectOption: setSelectedToken, matchWidth: true, theme: dropdownTheme })), _jsx("div", { className: classNames(Styles.tokenWarning, { [Styles.tokenWarningHidden]: !selectedToken }), children: renderTokenWarning() })] })); } function renderDisconnected() { return (_jsxs("div", { children: [_jsxs("label", { children: ["Access globe high-resolution 3D content, including photogrammetry, terrain, imagery and buildings. Bring your own data for tiling, hosting and streaming to ", viewState.terria.appName, "."] }), _jsx("div", { children: _jsx("button", { className: Styles.connectButton, onClick: connect, disabled: codeChallenge.hash === "", children: "Connect to Cesium ion" }) })] })); } function renderTokenWarning() { if (!selectedToken) return; const dangerousScopes = []; for (const scope of selectedToken.scopes ?? []) { // Only these scopes are "safe". if (scope !== "assets:read" && scope !== "geocode") { dangerousScopes.push(scope); } } if (dangerousScopes.length > 0) { return (_jsxs(_Fragment, { children: [_jsx("strong", { children: "DO NOT USE THIS TOKEN!" }), " It allows access to your", " ", _jsx("a", { href: "https://ion.cesium.com/tokens", target: "_blank", rel: "noreferrer", children: "Cesium ion account" }), " ", "using the following scopes that provide potentially sensitive information or allow changes to be made to your account:", " ", dangerousScopes.join(", ")] })); } if (!siteMatchesAllowedUrls(selectedToken)) { return (_jsxs(_Fragment, { children: ["This token cannot be used with this map because the map is not in the token's list of allowed URLs in your", " ", _jsx("a", { href: "https://ion.cesium.com/tokens", target: "_blank", rel: "noreferrer", children: "Cesium ion account" }), "."] })); } let numberOfAssetsAccessible = -1; if (selectedToken.assetIds) { numberOfAssetsAccessible = selectedToken.assetIds.length; } return (_jsxs(_Fragment, { children: ["This token allows access to", " ", _jsx("strong", { children: numberOfAssetsAccessible < 0 ? "every" : numberOfAssetsAccessible }), " ", numberOfAssetsAccessible <= 1 ? "asset" : "assets", " in your", " ", _jsx("a", { href: "https://ion.cesium.com/tokens", target: "_blank", rel: "noreferrer", children: "Cesium ion account" }), "."] })); } function renderAssetRow(asset) { return (_jsxs("tr", { children: [_jsx("td", { children: _jsx(ActionButton, { type: "button", onClick: addToMap.bind(undefined, viewState.terria, asset), title: t("catalogItem.add"), children: _jsx(Icon, { glyph: Icon.GLYPHS.add, className: Styles.addAssetButton }) }) }), _jsx("td", { children: asset.name }), _jsx("td", { children: asset.type })] }, asset.id)); } function connect() { const clientID = viewState.terria.configParameters.cesiumIonOAuth2ApplicationID; const redirectUri = URI("build/TerriaJS/cesium-ion-oauth2.html") .absoluteTo(window.location.href) .fragment("") .query("") .toString(); const codeChallengeValue = codeChallenge.value; const codeChallengeHash = codeChallenge.hash; const state = [...crypto.getRandomValues(new Uint8Array(16))] .map((x) => x.toString(16).padStart(2, "0")) .join(""); const uri = new URI("https://ion.cesium.com/oauth").addQuery({ response_type: "code", client_id: clientID, scope: "assets:read assets:list tokens:read profile:read", redirect_uri: redirectUri, state: state, code_challenge: codeChallengeHash, code_challenge_method: "S256" }); window["cesiumIonOAuth2_" + state] = function (code) { fetch("https://api.cesium.com/oauth/token", { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ grant_type: "authorization_code", client_id: clientID, code: code, redirect_uri: redirectUri, code_verifier: codeChallengeValue }) }) .then((response) => { return response.json(); }) .then((response) => { loginTokenPersistence.set(response.access_token); setLoginToken(response.access_token ?? ""); }); }; window.open(uri.toString(), "_blank", "popup=yes,width=600,height=600"); } function disconnect() { loginTokenPersistence.clear(); setLoginToken(""); setUserProfile(defaultUserProfile); } function addToMap(terria, asset, event) { // If the asset may be shared, the user must choose a suitable token. // If not, we can access the asset using the login token. const allowSharing = viewState.terria.configParameters.cesiumIonAllowSharingAddedAssets; const assetToken = allowSharing ? selectedToken : { token: loginToken, name: `${userProfile.username}'s login token`, uniqueName: `${userProfile.username}'s login token` }; if (!assetToken) return; const definition = createCatalogItemDefinitionFromAsset(terria, asset, assetToken); if (!definition) return; const newItem = upsertModelFromJson(CatalogMemberFactory, viewState.terria, "", terria.configParameters.cesiumIonAllowSharingAddedAssets ? CommonStrata.user : CommonStrata.definition, definition, {}).throwIfUndefined({ message: `An error occurred trying to add asset: ${asset.name}` }); const keepCatalogOpen = event.shiftKey || event.ctrlKey; addUserCatalogMember(viewState.terria, Promise.resolve(newItem)).then((addedItem) => { if (addedItem) { if (TimeVarying.is(addedItem)) { viewState.terria.timelineStack.addToTop(addedItem); } } if (!keepCatalogOpen) { viewState.closeCatalog(); } }); } function createCatalogItemDefinitionFromAsset(terria, asset, token) { let type = ""; const extras = {}; switch (asset.type) { case "3DTILES": type = "3d-tiles"; break; case "GLTF": type = "gltf"; // TODO extras.origin = { longitude: 0.0, latitude: 0.0, height: 0.0 }; break; case "IMAGERY": type = "ion-imagery"; break; case "TERRAIN": type = "cesium-terrain"; break; case "CZML": type = "czml"; break; case "KML": type = "kml"; break; case "GEOJSON": type = "geojson"; break; } if (type === "") return undefined; return { name: asset.name ?? "Unnamed", type: type, shareable: terria.configParameters.cesiumIonAllowSharingAddedAssets ?? false, description: asset.description ?? "", ionAssetId: asset.id ?? 0, ionAccessToken: token.token, info: [ { name: "Cesium ion Account", content: userProfile.username }, { name: "Cesium ion Token", content: token.name ?? token.id } ], ...extras }; } } function siteMatchesAllowedUrls(token) { if (!isDefined(token.allowedUrls)) { return true; } const current = new URI(window.location.href); for (const allowedUrl of token.allowedUrls) { let allowed; try { allowed = new URI(allowedUrl); } catch (_e) { continue; } const currentHostname = current.hostname(); const allowedHostname = allowed.hostname(); // Current hostname must either match the allowed one exactly, or be a subdomain of the allowed one. const hostnameValid = currentHostname === allowedHostname || (currentHostname.endsWith(allowedHostname) && currentHostname[currentHostname.length - allowedHostname.length - 1] === "."); if (!hostnameValid) continue; // If the current has a port, the allowed must match. if (current.port().length > 0 && current.port() !== allowed.port()) continue; // The current path must start with the allowed path. if (!current.path().startsWith(allowed.path())) continue; return true; } return false; } const CesiumIonConnectorObserver = observer(CesiumIonConnector); export default CesiumIonConnectorObserver; //# sourceMappingURL=CesiumIonConnector.js.map