diff --git a/packages/create-react/src/components/common/ProtectedRoute.tsx b/packages/create-react/src/components/common/ProtectedRoute.tsx
index 4cf001f..12df26c 100644
--- a/packages/create-react/src/components/common/ProtectedRoute.tsx
+++ b/packages/create-react/src/components/common/ProtectedRoute.tsx
@@ -1,25 +1,26 @@
import { Navigate } from 'react-router-dom';
-// import { useAuth0 } from '@auth0/auth0-react';
import { RoutePath } from '../../routes';
+import { useAuth0 } from '@auth0/auth0-react';
+import { AppContext } from '../../context';
+import { useContext } from 'react';
+import { useAuth } from '../../hooks/useAuth';
export default function ProtectedRoute({
children,
}: {
children: JSX.Element;
}) {
- // TODO(impl): Auth
- // const { isAuthenticated, isLoading } = useAuth0();
+ useAuth();
+ const { isAuthenticated, isLoading } = useAuth0();
+ const { oauth, accessToken } = useContext(AppContext);
- // if (!initialState.oauth) {
- // return children;
- // }
-
- const authenticated = true; //notAuthenticated = !isLoading && !isAuthenticated && !accessToken;
+ if (!oauth.enabled) {
+ return children;
+ }
- if (!authenticated) {
+ if (!isLoading && !isAuthenticated && !accessToken) {
return
;
}
- return children;
- // return !!accessToken ? children : null;
+ return accessToken ? children : null;
}
diff --git a/packages/create-react/src/components/views/Default.tsx b/packages/create-react/src/components/views/Default.tsx
index b9e8f14..39a1b35 100644
--- a/packages/create-react/src/components/views/Default.tsx
+++ b/packages/create-react/src/components/views/Default.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useMemo, useState } from 'react';
+import { useContext, useEffect, useMemo, useState } from 'react';
import { Map } from 'react-map-gl/maplibre';
import DeckGL from '@deck.gl/react';
@@ -11,6 +11,7 @@ import { Layers } from '../common/Layers';
import { FormulaWidget } from '../widgets/FormulaWidget';
import { CategoryWidget } from '../widgets/CategoryWidget';
import { useDebouncedState } from '../../hooks';
+import { AppContext } from '../../context';
const MAP_VIEW = new MapView({ repeat: true });
const MAP_STYLE =
@@ -31,6 +32,7 @@ const RADIO_COLORS: AccessorFunction
= colorCategories({
});
export default function Default() {
+ const { accessToken, apiBaseUrl } = useContext(AppContext);
const [filters, setFilters] = useState({} as Record);
const [attributionHTML, setAttributionHTML] = useState('');
const [viewState, setViewState] = useDebouncedState(INITIAL_VIEW_STATE, 200);
@@ -41,13 +43,14 @@ export default function Default() {
const data = useMemo(() => {
return vectorQuerySource({
- accessToken: import.meta.env.VITE_CARTO_ACCESS_TOKEN,
+ accessToken,
+ apiBaseUrl,
connectionName: 'carto_dw',
sqlQuery:
'SELECT * FROM `carto-demo-data.demo_tables.cell_towers_worldwide`',
filters,
});
- }, [filters]);
+ }, [accessToken, apiBaseUrl, filters]);
/****************************************************************************
* Layers (https://deck.gl/docs/api-reference/carto/overview#carto-layers)
diff --git a/packages/create-react/src/components/views/Login.tsx b/packages/create-react/src/components/views/Login.tsx
index 042bbc8..f842fae 100644
--- a/packages/create-react/src/components/views/Login.tsx
+++ b/packages/create-react/src/components/views/Login.tsx
@@ -1,3 +1,19 @@
+import { useAuth0 } from '@auth0/auth0-react';
+import { useContext } from 'react';
+import { AppContext } from '../../context';
+
export default function Login() {
- return Login
;
+ const { title, logo } = useContext(AppContext);
+ const { loginWithRedirect } = useAuth0();
+ return (
+
+ {logo && }
+ {title}
+ Discover the power of developing with React
+
+ Use your CARTO credentials
+
+ );
}
diff --git a/packages/create-react/src/components/views/Logout.tsx b/packages/create-react/src/components/views/Logout.tsx
new file mode 100644
index 0000000..b872c70
--- /dev/null
+++ b/packages/create-react/src/components/views/Logout.tsx
@@ -0,0 +1,9 @@
+import { useAuth0 } from '@auth0/auth0-react';
+
+export default function Logout() {
+ const { logout } = useAuth0();
+
+ logout();
+
+ return Logging out…
;
+}
diff --git a/packages/create-react/src/components/views/Secondary.tsx b/packages/create-react/src/components/views/Secondary.tsx
index 137f6ca..4739ca3 100644
--- a/packages/create-react/src/components/views/Secondary.tsx
+++ b/packages/create-react/src/components/views/Secondary.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useMemo, useState } from 'react';
+import { useContext, useEffect, useMemo, useState } from 'react';
import { Map } from 'react-map-gl/maplibre';
import DeckGL from '@deck.gl/react';
@@ -8,6 +8,7 @@ import { h3TableSource } from '@carto/api-client';
import { Legend } from '../common/Legend';
import { Layers } from '../common/Layers';
import { Card } from '../common/Card';
+import { AppContext } from '../../context';
const MAP_VIEW = new MapView({ repeat: true });
const MAP_STYLE =
@@ -28,6 +29,7 @@ const POP_COLORS: AccessorFunction = colorContinuous({
});
export default function Default() {
+ const { accessToken, apiBaseUrl } = useContext(AppContext);
const [attributionHTML, setAttributionHTML] = useState('');
const [viewState, setViewState] = useState(INITIAL_VIEW_STATE);
@@ -37,14 +39,15 @@ export default function Default() {
const data = useMemo(() => {
return h3TableSource({
- accessToken: import.meta.env.VITE_CARTO_ACCESS_TOKEN,
+ accessToken,
+ apiBaseUrl,
connectionName: 'carto_dw',
tableName:
'carto-demo-data.demo_tables.derived_spatialfeatures_usa_h3res8_v1_yearly_v2',
spatialDataColumn: 'h3',
aggregationExp: 'SUM(population) as population_sum',
});
- }, []);
+ }, [accessToken, apiBaseUrl]);
/****************************************************************************
* Layers (https://deck.gl/docs/api-reference/carto/overview#carto-layers)
diff --git a/packages/create-react/src/context.ts b/packages/create-react/src/context.ts
index 561335f..2dffc1c 100644
--- a/packages/create-react/src/context.ts
+++ b/packages/create-react/src/context.ts
@@ -7,26 +7,50 @@ export interface AppContextProps {
src: string;
alt: string;
};
- credentials: {
- accessToken: string;
- apiVersion?: string;
- apiBaseUrl?: string;
- };
+ accessToken: string;
+ setAccessToken: (token: string) => void;
+ apiVersion?: string;
+ apiBaseUrl?: string;
googleApiKey?: string;
googleMapId?: string;
accountsUrl?: string;
+ oauth: {
+ enabled: boolean;
+ domain: string;
+ clientId?: string;
+ organizationId?: string;
+ namespace: string;
+ scopes: string[];
+ audience: string;
+ authorizeEndPoint?: string;
+ };
}
export const DEFAULT_APP_CONTEXT = {
- title: '$title',
+ title: import.meta.env.VITE_APP_TITLE,
logo: {
src: cartoLogo,
alt: 'CARTO logo',
},
- credentials: {
- accessToken: import.meta.env.VITE_CARTO_ACCESS_TOKEN,
- },
+ accessToken: import.meta.env.VITE_CARTO_ACCESS_TOKEN,
+ setAccessToken: () => {},
+ apiBaseUrl: 'https://gcp-us-east1.api.carto.com',
accountsUrl: 'http://app.carto.com/',
+ oauth: {
+ enabled: import.meta.env.VITE_CARTO_AUTH_ENABLED === 'true',
+ domain: 'auth.carto.com',
+ clientId: import.meta.env.VITE_CARTO_AUTH_CLIENT_ID,
+ organizationId: import.meta.env.VITE_CARTO_AUTH_ORGANIZATION_ID, // Required for SSO.
+ namespace: 'http://app.carto.com/',
+ scopes: [
+ 'read:current_user',
+ 'read:connections',
+ 'read:maps',
+ 'read:account',
+ ],
+ audience: 'carto-cloud-native-api',
+ authorizeEndPoint: 'https://carto.com/oauth2/authorize', // Only valid if keeping https://localhost:3000/oauthCallback
+ },
};
export const AppContext = createContext(DEFAULT_APP_CONTEXT);
diff --git a/packages/create-react/src/hooks/useAuth.ts b/packages/create-react/src/hooks/useAuth.ts
new file mode 100644
index 0000000..6c9eefb
--- /dev/null
+++ b/packages/create-react/src/hooks/useAuth.ts
@@ -0,0 +1,54 @@
+import { useCallback, useContext, useEffect, useMemo } from 'react';
+import { useAuth0 } from '@auth0/auth0-react';
+import { useSearchParams } from 'react-router-dom';
+import { AppContext } from '../context';
+
+const FORCE_LOGIN_PARAM = 'forceLogin';
+
+export function useAuth() {
+ const { setAccessToken, accountsUrl, oauth } = useContext(AppContext);
+ const { isAuthenticated, getAccessTokenSilently, user, loginWithRedirect } =
+ useAuth0();
+ const [searchParams] = useSearchParams();
+
+ const organizationId = oauth.organizationId || '';
+ const namespace = oauth.namespace;
+ const hasForceLogin = searchParams.has(FORCE_LOGIN_PARAM);
+
+ const getAccessToken = useCallback(async () => {
+ setAccessToken(await getAccessTokenSilently());
+ }, [setAccessToken, getAccessTokenSilently]);
+
+ const userMetadata = useMemo(() => {
+ if (!user) return;
+ return user[`${namespace}user_metadata`];
+ }, [user, namespace]);
+
+ const redirectAccountUri = useMemo(() => {
+ return `${accountsUrl}${organizationId ? `sso/${organizationId}` : ''}`;
+ }, [accountsUrl, organizationId]);
+
+ useEffect(() => {
+ if (hasForceLogin) {
+ // if FORCE_LOGIN_PARAM is set a relogin is required to refresh userMetadata
+ loginWithRedirect();
+ } else if (isAuthenticated && userMetadata) {
+ getAccessToken();
+ } else if (isAuthenticated) {
+ // No organizations associated with the user, we need to redirect to app.carto.com to complete the signup process
+ const searchParams = new URLSearchParams({
+ redirectUri: `${window.location.origin}?${FORCE_LOGIN_PARAM}=true`,
+ });
+ window.location.href = `${redirectAccountUri}?${searchParams}`;
+ }
+ }, [
+ hasForceLogin,
+ getAccessToken,
+ isAuthenticated,
+ loginWithRedirect,
+ redirectAccountUri,
+ userMetadata,
+ ]);
+
+ return;
+}
diff --git a/packages/create-react/src/main.tsx b/packages/create-react/src/main.tsx
index 571db90..f580d6f 100644
--- a/packages/create-react/src/main.tsx
+++ b/packages/create-react/src/main.tsx
@@ -3,7 +3,6 @@ import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import '@carto/create-common/style.css';
-// TODO: Auth provider.
ReactDOM.createRoot(document.getElementById('root')!).render(
diff --git a/packages/create-react/src/routes.tsx b/packages/create-react/src/routes.tsx
index 0282482..a804748 100644
--- a/packages/create-react/src/routes.tsx
+++ b/packages/create-react/src/routes.tsx
@@ -11,8 +11,9 @@ const Secondary = lazy(() => import('./components/views/Secondary'));
const NotFound = lazy(() => import('./components/views/NotFound'));
// eslint-disable-next-line react-refresh/only-export-components
const Login = lazy(() => import('./components/views/Login'));
+// eslint-disable-next-line react-refresh/only-export-components
+const Logout = lazy(() => import('./components/views/Logout'));
-// TODO: /logout ?
export const RoutePath: Record = {
DEFAULT: '/',
US_POPULATION: '/usa-population',
@@ -41,6 +42,7 @@ export const routes: RouteObject[] = [
],
},
{ path: RoutePath.LOGIN, element: },
+ { path: RoutePath.LOGOUT, element: },
{ path: '*', element: },
];
diff --git a/packages/create-vue/public/carto.svg b/packages/create-vue/public/carto.svg
index 3cbdfc1..bfe5851 100644
--- a/packages/create-vue/public/carto.svg
+++ b/packages/create-vue/public/carto.svg
@@ -1 +1,9 @@
-
+