Skip to content

Commit

Permalink
Street smart openid support (#10817)
Browse files Browse the repository at this point in the history
  • Loading branch information
offtherailz authored Feb 17, 2025
1 parent 6863a19 commit 55ab964
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 27 deletions.
8 changes: 4 additions & 4 deletions binary/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,15 @@
<id>get-untar-jre-copy-db</id>
<phase>package</phase>
<configuration>
<tasks>
<target>
<echo message="Downloading JRE..."/>
<get src="https://www.dropbox.com/scl/fi/2rjaso3cxgab0jdjxwpjr/jre.tar.gz?rlkey=1wrf6mcemn5d2e88t8u0wxbk3&amp;dl=1"
dest="${project.build.directory}/jre.tar.gz"/>
<echo message="Untar JRE..."/>
<gunzip src="${project.build.directory}/jre.tar.gz"
dest="${project.build.directory}/jre.tar"/>
<untar src="${project.build.directory}/jre.tar" dest="${project.build.directory}/jre"/>
</tasks>
</target>
</configuration>
<goals>
<goal>run</goal>
Expand All @@ -132,12 +132,12 @@
<phase>package</phase>
<configuration>
<!-- remove some unwanted dependencies that get pulled over -->
<tasks>
<target>
<delete>
<fileset dir="${project.build.directory}/dependency"
includes="servlet-api-*.jar,core*.jar"/>
</delete>
</tasks>
</target>
</configuration>
<goals>
<goal>run</goal>
Expand Down
60 changes: 54 additions & 6 deletions web/client/plugins/StreetView/StreetView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,60 @@ const StreetViewPluginContainer = connect(() => ({}), {
* - `googleAPIKey` (for retro-compatibility only)
* - `cyclomedia` provider: The API key is mandatory and can be configured only in the plugin configuration. It is not possible to configure it globally in `localConfig.json`, in `apiKeys.cyclomediaAPIKey`.
* @property {string} providerSettings The settings specific for the provider. Depending on the `provider` property, the following settings are available:
* - `cyclomedia` provider:
* - `providerSettings.StreetSmartApiURL` (optional). The URL of the StreetSmart API. Default: `https://streetsmart.cyclomedia.com/api/v23.7/StreetSmartApi.js`.
* - `providerSettings.srs` (optional). Coordinate reference system code to use for the API. Default: `EPSG:4326`. Note that the SRS used here must be supported by the StreetSmart API **and** defined in `localConfig.json` file, in `projectionDefs`.
*
* Generally speaking, you should prefer general settings in `localConfig.json` over the plugin configuration, in order to reuse the same configuration for default viewer and all the contexts, automatically. This way you will not need to configure the `apiKey` in every context.
* <br>**Important**: You can use only **one** API-key for a MapStore instance. The api-key can be configured replicated in every plugin configuration or using one of the unique global settings (suggested) in `localConfig.json`). @see {@link https://github.com/googlemaps/js-api-loader/issues/5|here} and @see {@link https://github.com/googlemaps/js-api-loader/issues/100|here}
* - `cyclomedia` provider. The `cyclomedia` (StreetSmart) provider allows a set of possible setup. The minimal one allows can include the `apiKey`. In this case the credentials will be asked to the user when the plugin is activated.
* Here an example of the full plugin configuration:
* ```json
* {
* "provider": "cyclomedia",
* "apiKey": "<your-api-key>",
* "providerSettings": {
* "srs": "EPSG:7791"
* }
* ```
* A more complex configuration allows to use the Oauth login (and also pre-configure `credentials`, **see Important security note about this**).
* In this case the `providerSettings` must include `initOptions` with `loginOauth=true`, `clientId`, `loginRedirectUri` and `logoutRedirectUri`. Moreover:
* - the `clientId` must be registered in the Cyclomedia API.
* - the pages indicated as `loginRedirectUri` and `logoutRedirectUri` must be
* - accessible by the user
* - configured in the Cyclomedia API for the instance deployed
* - the content of the pages have to contain the JS code to handle the login callback from the API, as indicated in [StreetSmart API documentation](https://docs.cyclomedia.com/StreetSmart/documentation/#oauth).
* For more information about the Oauth login, see the [StreetSmart API documentation](https://docs.cyclomedia.com/StreetSmart/documentation/)
* Here an example, and below the details for every property:
* ```json
* {
* "provider": "cyclomedia",
* "apiKey": "<your-api-key>",
* "providerSettings": {
* "srs": "EPSG:7791",
* "credentials": {
* "username": "<your-username>",
* "password": "<your-password>"
* },
* "initOptions": {
* "clientId": "<your-client-id>",
* "loginOauth": true,
* "loginRedirectUri": "<url-to-cm-login.html>",
* "logoutRedirectUri": "<url-to-cm-logout.html>"
* }
* }
* ```
* - `providerSettings` (optional). The settings specific for the provider. It is an object with the following properties:
* - `providerSettings.StreetSmartApiURL` (optional). The URL of the StreetSmart API. Default: `https://streetsmart.cyclomedia.com/api/v23.7/StreetSmartApi.js`.
* - `providerSettings.srs` (optional). Coordinate reference system code to use for the API. Default: `EPSG:4326`. Note that the SRS used here must be supported by the StreetSmart API **and** defined in `localConfig.json` file, in `projectionDefs`.
* - `providerSettings.credentials` (optional). The credentials to store for the Cyclomedia API. It is an object with `username` and `password` properties.
* - **Important Note**: The plugin provides the possibility to configure the credentials in the plugin configuration, but in this case you have to be aware
* that this will make these credentials potentially accessible to all the user that can access the context, or if set in `localConfig.json`, to all the users of the application.
* This settings should be used only in case the context or the application are shared within a restricted group of users, and this doesn't represent a security issue.
* It is up to you to evaluate the security implications of this choice.
* - `providerSettings.showLogout` (optional). If true, the plugin will show a login button (only if `initOptions.loginOauth` is set to true). Default: `true`.
* - `initOptions` (optional). The options to pass to the StreetSmart API. Default: `{}`. It can contain in particular the options to enable login Oauth:
* - `initOptions.loginOauth` (optional). If true, instead of username and password provided by the user or configured in the plugin, the application will use the Oauth login. In this case also the `clientId`, `loginRedirectUri` and `logoutRedirectUri` must be specified in the `localConfig.json` file.
* - `initOptions.clientId` (optional). The client ID for the Oauth login. It is mandatory if `loginOauth` is true.
* - `initOptions.loginRedirectUri` (optional). The redirect URI after login. It is mandatory if `loginOauth` is true. The page must be accessible and configured in StreetSmart API for the instance.
* - `initOptions.logoutRedirectUri` (optional). The redirect URI after logout. It is mandatory if `loginOauth` is true. The page must be configured to handle the login callback StreetSmart API for the instance.
* - `initOptions.doOAuthLogoutOnDestroy` (optional). If true, the plugin will logout from the StreetSmart API when the plugin is destroyed. Default: `false`.
* Generally speaking, you should prefer general settings in `localConfig.json` over the plugin configuration, in order to reuse the same configuration for default viewer and all the contexts, automatically. This way you will not need to configure the `apiKey` in every context.
* <br>**Important**: You can use only **one** API-key for a MapStore instance. The api-key can be configured replicated in every plugin configuration or using one of the unique global settings (suggested) in `localConfig.json`). @see {@link https://github.com/googlemaps/js-api-loader/issues/5|here} and @see {@link https://github.com/googlemaps/js-api-loader/issues/100|here}
* @property {boolean} [cfg.useDataLayer=true] If true, adds to the map a layer for street view data availability when the plugin is turned on.
* @property {object} [cfg.dataLayerConfig] configuration for the data layer. By default `{provider: 'custom', type: "tileprovider", url: "https://mts1.googleapis.com/vt?hl=en-US&lyrs=svv|cb_client:apiv3&style=40,18&x={x}&y={y}&z={z}"}`
* @property {object} [cfg.panoramaOptions] options to configure the panorama. {@link https://developers.google.com/maps/documentation/javascript/reference/street-view#panoramaOptions|Reference for google maps API}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,38 @@
import React, {useState, useEffect, useRef} from 'react';
import {isEmpty} from 'lodash';
import Message from '../../../../components/I18N/Message';

import { isProjectionAvailable } from '../../../../utils/ProjectionUtils';
import { reproject } from '../../../../utils/CoordinatesUtils';


import { getCredentials as getStoredCredentials, setCredentials as setStoredCredentials } from '../../../../utils/SecurityUtils';
import { CYCLOMEDIA_CREDENTIALS_REFERENCE } from '../../constants';
import { Alert, Button } from 'react-bootstrap';

import { Alert, Button, Glyphicon } from 'react-bootstrap';
import withConfirm from '../../../../components/misc/withConfirm';
import withTooltip from '../../../../components/misc/enhancers/tooltip';
const CTButton = withConfirm(withTooltip(Button));
import CyclomediaCredentials from './Credentials';
import EmptyStreetView from '../EmptyStreetView';
const PROJECTION_NOT_AVAILABLE = "Projection not available";
const isInvalidCredentials = (error) => {
return error?.message?.indexOf?.("code 401");
};
function checkPopupBlocked(err = "") {
const popupErr = "Popup blocked. Please allow popups for this site and refresh the page.";
if (err?.message?.indexOf?.("not logged in") >= 0) {
const win = window.open('', '_blank', 'width=1,height=1');
if (win && win.closed) {
return new Error(popupErr);
} else if (win) {
win.close();
return false;
}
return new Error(popupErr);

}
return false;
}
/**
* Parses the error message to show to the user in the alert an user friendly message
* @private
Expand All @@ -39,9 +58,11 @@ const getErrorMessage = (error, msgParams = {}) => {
* @param {boolean} props.initialized true if the API is initialized
* @param {object} props.StreetSmartApi the StreetSmartApi object
* @param {boolean} props.mapPointVisible true if the map point are visible at the current level of zoom.
* @param {boolean} props.loggingOut true if the user is logging out
* @param {function} props.onClose the function to call when the user closes the component
* @returns {JSX.Element} the component rendering
*/
const EmptyView = ({initializing, initialized, StreetSmartApi, mapPointVisible}) => {
const EmptyView = ({initializing, initialized, StreetSmartApi, mapPointVisible, loggingOut, onClose}) => {
if (initialized && !mapPointVisible) {
return (
<EmptyStreetView description={<Message msgId="streetView.cyclomedia.zoomIn" />} />
Expand All @@ -63,6 +84,14 @@ const EmptyView = ({initializing, initialized, StreetSmartApi, mapPointVisible})
<EmptyStreetView loading description={<Message msgId="streetView.cyclomedia.loadingAPI" />} />
);
}
if (loggingOut) {
return (<EmptyStreetView description={<>
<div><Message msgId="streetView.cyclomedia.loggingOut" /></div>
<Button onClick={() => {
onClose();
}}>Close</Button>
</>} />);
}
return null;
};

Expand All @@ -85,7 +114,7 @@ const EmptyView = ({initializing, initialized, StreetSmartApi, mapPointVisible})
* @returns {JSX.Element} the component rendering
*/

const CyclomediaView = ({ apiKey, style, location = {}, setPov = () => {}, setLocation = () => {}, mapPointVisible, providerSettings = {}, refreshLayer = () => {}}) => {
const CyclomediaView = ({ apiKey, style, location = {}, setPov = () => {}, setLocation = () => {}, mapPointVisible, providerSettings = {}, refreshLayer = () => {}, onClose = () => {}}) => {
const StreetSmartApiURL = providerSettings?.StreetSmartApiURL ?? "https://streetsmart.cyclomedia.com/api/v23.7/StreetSmartApi.js";
const scripts = providerSettings?.scripts ?? `
<script type="text/javascript" src="https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
Expand All @@ -109,9 +138,15 @@ const CyclomediaView = ({ apiKey, style, location = {}, setPov = () => {}, setLo
const [initialized, setInitialized] = useState(false);
const [reload, setReload] = useState(1);
const [error, setError] = useState(null);

// gets the credentials from the storage
const initialCredentials = getStoredCredentials(CYCLOMEDIA_CREDENTIALS_REFERENCE);
const [reloadAllowed, setReloadAllowed] = useState(false);
const [loggingOut, setLoggingOut] = useState(false);
// gets the credentials from the storage or from configuration.
const hasConfiguredCredentials = providerSettings?.credentials;
const isConfiguredOauth = initOptions?.loginOauth;
const showLogout = providerSettings?.showLogout ?? true;
const initialCredentials =
isEmpty(getStoredCredentials(CYCLOMEDIA_CREDENTIALS_REFERENCE)) ?
providerSettings?.credentials ?? {} : getStoredCredentials(CYCLOMEDIA_CREDENTIALS_REFERENCE);
const [credentials, setCredentials] = useState(initialCredentials);
const [showCredentialsForm, setShowCredentialsForm] = useState(!credentials?.username || !credentials?.password); // determines to show the credentials form
const {username, password} = credentials ?? {};
Expand Down Expand Up @@ -168,21 +203,43 @@ const CyclomediaView = ({ apiKey, style, location = {}, setPov = () => {}, setLo
setInitializing(true);
StreetSmartApi.init({
targetElement,
username,
password,

apiKey,
loginOauth: false,
srs: srs,
locale: 'en-us',
...initOptions
...initOptions,
...(isConfiguredOauth
? { }
: {
username,
password
} )
}).then(function() {
setInitializing(false);
setInitialized(true);
setError(null);
}).catch(function(err) {
setInitializing(false);
if (isConfiguredOauth) {
// check if the error is related to the oauth login, in particular to popup blocked.
// check if popup is blocked and show a message to the user, because the street smart api error do not provide a clear message
const blockedPopup = checkPopupBlocked(err);
if ( blockedPopup ) {
console.error('Cyclomedia API: init: error: ' + blockedPopup);
setError(blockedPopup);
setReloadAllowed(true);
return;
}

}
setError(err);
if (err) {console.error('Cyclomedia API: init: error: ' + err);}
setReloadAllowed(true);
if (err) {
console.error('Cyclomedia API: init: error: ' + err);
}


});
return () => {
try {
Expand Down Expand Up @@ -217,7 +274,8 @@ const CyclomediaView = ({ apiKey, style, location = {}, setPov = () => {}, setLo
setLocation({
latLng: {
lat,
lng
lng,
h: recording?.xyz?.[2] || 0
},
properties: {
...recording,
Expand Down Expand Up @@ -285,15 +343,40 @@ const CyclomediaView = ({ apiKey, style, location = {}, setPov = () => {}, setLo
</body>
</html>`;
return (<>
{<CyclomediaCredentials
{!hasConfiguredCredentials && <CyclomediaCredentials
key="credentials"
showCredentialsForm={showCredentialsForm}
setShowCredentialsForm={setShowCredentialsForm}
credentials={credentials}
setCredentials={(newCredentials) => {
setCredentials(newCredentials);
}}/>}
{showEmptyView ? <EmptyView key="empty-view" StreetSmartApi={StreetSmartApi} style={style} initializing={initializing} initialized={initialized} mapPointVisible={mapPointVisible}/> : null}
{showLogout
&& initialized
&& isConfiguredOauth
&& !error
&& (<div style={{textAlign: "right"}}>
<CTButton
key="logout"
confirmContent={<Message msgId="streetView.cyclomedia.confirmLogout" />}
tooltipId="streetView.cyclomedia.logout"
onClick={() => {
StreetSmartApi?.destroy?.({targetElement, loginOauth: true});
setInitialized(false);
setLoggingOut(true);
}}>
<Glyphicon glyph="log-out" />&nbsp;
</CTButton></div>)}
{showEmptyView
? <EmptyView key="empty-view"
StreetSmartApi={StreetSmartApi}
style={style}
initializing={initializing}
initialized={initialized}
loggingOut={loggingOut}
onClose={onClose}
mapPointVisible={mapPointVisible}/>
: null}
<iframe key="iframe" ref={viewer} onLoad={() => {
setTargetElement(viewer.current?.contentDocument.querySelector('#ms-street-smart-viewer-container'));
setStreetSmartApi(viewer.current?.contentWindow.StreetSmartApi);
Expand All @@ -303,9 +386,10 @@ const CyclomediaView = ({ apiKey, style, location = {}, setPov = () => {}, setLo
<Alert bsStyle="danger" style={{...style, textAlign: 'center', alignContent: 'center', display: showError ? 'block' : 'none'}} key="error">
<Message msgId="streetView.cyclomedia.errorOccurred" />
{getErrorMessage(error, {srs})}
{initialized ? <div><Button
{initialized || reloadAllowed ? <div><Button
onClick={() => {
setError(null);
setReloadAllowed(false);
try {
setReload(reload + 1);
} catch (e) {
Expand All @@ -314,6 +398,20 @@ const CyclomediaView = ({ apiKey, style, location = {}, setPov = () => {}, setLo
}}>
<Message msgId="streetView.cyclomedia.reloadAPI"/>
</Button></div> : null}
{
isConfiguredOauth
&& !showCredentialsForm
&& !initialized
&& (<CTButton
key="logout"
confirmContent={<Message msgId="streetView.cyclomedia.confirmLogout" />}
tooltipId="streetView.cyclomedia.tryForceLogout"
onClick={() => {
StreetSmartApi?.destroy?.({targetElement, loginOauth: true});
}}>
<Glyphicon glyph="log-out" />&nbsp;<Message msgId="streetView.cyclomedia.logout" />
</CTButton>)
}
</Alert>
</>);
};
Expand Down
Loading

0 comments on commit 55ab964

Please sign in to comment.