terriajs
Version:
Geospatial data visualization platform.
441 lines (440 loc) • 19.2 kB
JavaScript
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