Skip to content

Commit 16c9582

Browse files
authored
Merge pull request #23 from developmentseed/feature/destine
Destine data
2 parents aa3ef0d + b0f550e commit 16c9582

31 files changed

+1053
-74
lines changed

.env

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
APP_TITLE=GFTS
2-
APP_DESCRIPTION=GFTS point cloud
2+
APP_DESCRIPTION=Global Fish Tracking System
33

44
#MAPBOX_TOKEN=
55

6-
#DATA_API=''
6+
#DATA_API=''
7+
REACT_APP_KEYCLOAK_URL
8+
REACT_APP_KEYCLOAK_REALM
9+
REACT_APP_KEYCLOAK_CLIENT_ID

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "static/destine"]
2+
path = static/destine
3+
url = git@github.com:developmentseed/gfts-destine-data.git

README.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,20 @@ Install Node modules:
2323
yarn install
2424
```
2525

26+
### Initialize the Submodules
27+
The destine data repo is at: https://github.com/developmentseed/gfts-detine-data
28+
If you cloned the repository with `git clone --recursive`, you can skip this step. Otherwise, run:
29+
30+
```
31+
git submodule update --init --recursive
32+
```
33+
34+
#### How the submodules are used
35+
The submodule is a private repository that is used to include the Destine data which cannot be publicly shared.
36+
37+
When the app is built, the submodule is copied to the `destine` folder in the `dist` directory. The server will then handle requests to the `destine` folder and block the however is not authenticated.
38+
**Therefore, even though the data looks like it is freely available, it is actually protected by the server.**
39+
2640
## Usage
2741

2842
### Config files
@@ -33,8 +47,8 @@ These files are used to simplify the configuration of the app and should not con
3347
Run the project locally by copying the `.env` to `.env.local` and setting the following environment variables:
3448

3549

36-
| VAR | Description |
37-
| --- | --- |
50+
| VAR | Description |
51+
| -------------- | ------------------------ |
3852
| `MAPBOX_TOKEN` | Mapbox token for the map |
3953

4054
### Starting the app

app/components/auth/context.tsx

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import React, {
2+
createContext,
3+
useContext,
4+
useEffect,
5+
useRef,
6+
useState
7+
} from 'react';
8+
import Keycloak from 'keycloak-js';
9+
10+
const url = process.env.REACT_APP_KEYCLOAK_URL;
11+
const realm = process.env.REACT_APP_KEYCLOAK_REALM;
12+
const clientId = process.env.REACT_APP_KEYCLOAK_CLIENT_ID;
13+
14+
const isAuthEnabled = !!(url && realm && clientId);
15+
16+
const keycloak: Keycloak | undefined = isAuthEnabled
17+
? new (Keycloak as any)({
18+
url,
19+
realm,
20+
clientId
21+
})
22+
: undefined;
23+
24+
interface UserProfile {
25+
groups: string[];
26+
username: string;
27+
}
28+
29+
export type KeycloakContextProps = {
30+
initStatus: 'loading' | 'success' | 'error';
31+
isLoading: boolean;
32+
profile?: UserProfile;
33+
hasDPADAccess: boolean;
34+
} & (
35+
| {
36+
keycloak: Keycloak;
37+
isEnabled: true;
38+
}
39+
| {
40+
keycloak: undefined;
41+
isEnabled: false;
42+
}
43+
);
44+
45+
const KeycloakContext = createContext<KeycloakContextProps>({
46+
initStatus: 'loading',
47+
isEnabled: isAuthEnabled
48+
} as KeycloakContextProps);
49+
50+
export const KeycloakProvider = (props: { children: React.ReactNode }) => {
51+
const [initStatus, setInitStatus] =
52+
useState<KeycloakContextProps['initStatus']>('loading');
53+
const [profile, setProfile] = useState<UserProfile | undefined>();
54+
55+
const wasInit = useRef(false);
56+
57+
useEffect(() => {
58+
async function initialize() {
59+
if (!keycloak) return;
60+
// Keycloak can only be initialized once. This is a workaround to avoid
61+
// multiple initialization attempts, specially by React double rendering.
62+
if (wasInit.current) return;
63+
wasInit.current = true;
64+
65+
try {
66+
await keycloak.init({
67+
// onLoad: 'login-required',
68+
onLoad: 'check-sso',
69+
checkLoginIframe: false
70+
});
71+
if (keycloak.authenticated) {
72+
// const profile =
73+
// await (keycloak.loadUserProfile() as unknown as Promise<Keycloak.KeycloakProfile>);
74+
setProfile({
75+
groups: keycloak.idTokenParsed?.access_group || [],
76+
username: keycloak.idTokenParsed?.preferred_username || ''
77+
});
78+
}
79+
80+
setInitStatus('success');
81+
} catch (err) {
82+
setInitStatus('error');
83+
// eslint-disable-next-line no-console
84+
console.error('Failed to initialize keycloak adapter:', err);
85+
}
86+
}
87+
initialize();
88+
}, []);
89+
90+
const base = {
91+
initStatus,
92+
isLoading: isAuthEnabled && initStatus === 'loading',
93+
profile,
94+
hasDPADAccess: !!(
95+
isAuthEnabled &&
96+
keycloak?.authenticated &&
97+
profile?.groups?.includes('DPAD_Direct_Access')
98+
)
99+
};
100+
101+
return (
102+
<KeycloakContext.Provider
103+
value={
104+
isAuthEnabled
105+
? {
106+
...base,
107+
keycloak: keycloak!,
108+
isEnabled: true
109+
}
110+
: {
111+
...base,
112+
keycloak: undefined,
113+
isEnabled: false
114+
}
115+
}
116+
>
117+
{props.children}
118+
</KeycloakContext.Provider>
119+
);
120+
};
121+
122+
export const useKeycloak = () => {
123+
const ctx = useContext(KeycloakContext);
124+
125+
if (!ctx) {
126+
throw new Error('useKeycloak must be used within a KeycloakProvider');
127+
}
128+
129+
return ctx;
130+
};

app/components/auth/userInfo.tsx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import React from 'react';
2+
import { Avatar, IconButton, Tooltip } from '@chakra-ui/react';
3+
import { CollecticonLogin } from '@devseed-ui/collecticons-chakra';
4+
5+
import { useKeycloak } from './context';
6+
import SmartLink from '$components/common/smart-link';
7+
8+
export function UserInfo() {
9+
const { profile, isLoading, isEnabled, keycloak } = useKeycloak();
10+
11+
if (!isEnabled) {
12+
return null;
13+
}
14+
15+
const isAuthenticated = keycloak.authenticated;
16+
17+
if (!isAuthenticated || !profile || isLoading) {
18+
return (
19+
<Tooltip hasArrow label='Login' placement='right' bg='base.500'>
20+
<IconButton
21+
aria-label='Login'
22+
size='sm'
23+
variant='ghost'
24+
colorScheme='base'
25+
_active={{ bg: 'base.100a' }}
26+
icon={<CollecticonLogin />}
27+
onClick={() => {
28+
if (!isLoading) {
29+
keycloak.login({
30+
redirectUri: window.location.href
31+
});
32+
}
33+
}}
34+
/>
35+
</Tooltip>
36+
);
37+
}
38+
39+
const username = profile.username;
40+
41+
return (
42+
<Tooltip hasArrow label='Logout' placement='right' bg='base.500'>
43+
<SmartLink
44+
to='/'
45+
display='block'
46+
onClick={(e) => {
47+
e.preventDefault();
48+
keycloak.clearToken();
49+
keycloak.logout({
50+
redirectUri: window.location.href
51+
});
52+
}}
53+
>
54+
<Avatar
55+
size='sm'
56+
name={username}
57+
bg='secondary.500'
58+
color='white'
59+
borderRadius='4px'
60+
/>
61+
</SmartLink>
62+
</Tooltip>
63+
);
64+
}

app/components/common/app-context.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,17 @@ interface IndividualContextProps {
1313
setCurrentPDFIndex: React.Dispatch<React.SetStateAction<number>>;
1414
}
1515

16+
type DestineLayerType = 'temperature' | 'salinity';
17+
1618
interface SpeciesContextProps {
1719
group: SpeciesGroup | null;
1820
setGroup: React.Dispatch<React.SetStateAction<SpeciesGroup | null>>;
21+
destineLayer: DestineLayerType | null;
22+
setDestineLayer: React.Dispatch<
23+
React.SetStateAction<DestineLayerType | null>
24+
>;
25+
destineYear: number | undefined;
26+
setDestineYear: React.Dispatch<React.SetStateAction<number | undefined>>;
1927
}
2028

2129
interface AppContextProps {
@@ -39,10 +47,18 @@ export const AppContextProvider: React.FC<{ children: ReactNode }> = ({
3947
};
4048

4149
const [speciesGroup, setSpeciesGroup] = useState<SpeciesGroup | null>(null);
50+
const [destineLayer, setDestineLayer] = useState<DestineLayerType | null>(
51+
null
52+
);
53+
const [destineYear, setDestineYear] = useState<number>();
4254

4355
const speciesContextValue = {
4456
group: speciesGroup,
45-
setGroup: setSpeciesGroup
57+
setGroup: setSpeciesGroup,
58+
destineLayer,
59+
setDestineLayer,
60+
destineYear,
61+
setDestineYear
4662
};
4763

4864
useLayoutEffect(() => {
@@ -80,4 +96,4 @@ export const useSpeciesContext = (): SpeciesContextProps => {
8096
);
8197
}
8298
return context.species;
83-
};
99+
};

0 commit comments

Comments
 (0)