Skip to content

Commit 65c9292

Browse files
Feat: connect gbfs validator to api (#1455)
* gbfs validator store state setup * gbfs validator new env variable for endpoint * gbfs validator integration with store * gbfs validator search persistent state
1 parent 9b95e30 commit 65c9292

File tree

13 files changed

+526
-162
lines changed

13 files changed

+526
-162
lines changed

.github/workflows/web-app-deployer.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ jobs:
120120
echo "CYPRESS_PWD=${{ secrets.DEV_CYPRESS_PWD }}" >> $GITHUB_ENV
121121
echo "REACT_APP_REMOTE_CONFIG_MINIMUM_FETCH_INTERVAL_MILLI=300000" >> $GITHUB_ENV
122122
echo "REACT_APP_FEED_API_BASE_URL=https://api-dev.mobilitydatabase.org" >> $GITHUB_ENV
123+
echo "REACT_APP_GBFS_VALIDATOR_API_BASE_URL=https://dev.gbfs.api.mobilitydatabase.org" >> $GITHUB_ENV
123124
124125
- name: Populate Variables
125126
working-directory: web-app
@@ -220,6 +221,7 @@ jobs:
220221
echo "REACT_APP_RECAPTCHA_SITE_KEY=${{ secrets.QA_REACT_APP_RECAPTCHA_SITE_KEY }}" >> $GITHUB_ENV
221222
echo "REACT_APP_REMOTE_CONFIG_MINIMUM_FETCH_INTERVAL_MILLI=300000" >> $GITHUB_ENV
222223
echo "REACT_APP_FEED_API_BASE_URL=https://api-qa.mobilitydatabase.org" >> $GITHUB_ENV
224+
echo "REACT_APP_GBFS_VALIDATOR_API_BASE_URL=https://dev.gbfs.api.mobilitydatabase.org" >> $GITHUB_ENV
223225
elif [[ ${{ inputs.FIREBASE_PROJECT }} == 'prod' ]]; then
224226
echo "Setting FIREBASE_PROJECT to 'prod'"
225227
echo "FIREBASE_PROJECT=prod" >> $GITHUB_ENV
@@ -232,6 +234,7 @@ jobs:
232234
echo "REACT_APP_RECAPTCHA_SITE_KEY=${{ secrets.PROD_REACT_APP_RECAPTCHA_SITE_KEY }}" >> $GITHUB_ENV
233235
echo "REACT_APP_REMOTE_CONFIG_MINIMUM_FETCH_INTERVAL_MILLI=3600000" >> $GITHUB_ENV
234236
echo "REACT_APP_FEED_API_BASE_URL=https://api.mobilitydatabase.org" >> $GITHUB_ENV
237+
echo "REACT_APP_GBFS_VALIDATOR_API_BASE_URL=https://dev.gbfs.api.mobilitydatabase.org" >> $GITHUB_ENV
235238
else
236239
echo "Setting FIREBASE_PROJECT to 'dev'"
237240
echo "FIREBASE_PROJECT=dev" >> $GITHUB_ENV
@@ -244,12 +247,13 @@ jobs:
244247
echo "REACT_APP_RECAPTCHA_SITE_KEY=${{ secrets.DEV_REACT_APP_RECAPTCHA_SITE_KEY }}" >> $GITHUB_ENV
245248
echo "REACT_APP_REMOTE_CONFIG_MINIMUM_FETCH_INTERVAL_MILLI=300000" >> $GITHUB_ENV
246249
echo "REACT_APP_FEED_API_BASE_URL=https://api-dev.mobilitydatabase.org" >> $GITHUB_ENV
250+
echo "REACT_APP_GBFS_VALIDATOR_API_BASE_URL=https://dev.gbfs.api.mobilitydatabase.org" >> $GITHUB_ENV
247251
fi
248252
249253
- name: Populate Variables
250254
working-directory: web-app
251255
run: |
252-
../scripts/replace-variables.sh -in_file src/.env.rename_me -out_file src/.env.${{ inputs.FIREBASE_PROJECT }} -variables REACT_APP_FIREBASE_API_KEY,REACT_APP_FIREBASE_AUTH_DOMAIN,REACT_APP_FIREBASE_PROJECT_ID,REACT_APP_FIREBASE_STORAGE_BUCKET,REACT_APP_FIREBASE_MESSAGING_SENDER_ID,REACT_APP_FIREBASE_APP_ID,REACT_APP_RECAPTCHA_SITE_KEY,REACT_APP_GOOGLE_ANALYTICS_ID,REACT_APP_REMOTE_CONFIG_MINIMUM_FETCH_INTERVAL_MILLI,REACT_APP_FEED_API_BASE_URL
256+
../scripts/replace-variables.sh -in_file src/.env.rename_me -out_file src/.env.${{ inputs.FIREBASE_PROJECT }} -variables REACT_APP_FIREBASE_API_KEY,REACT_APP_FIREBASE_AUTH_DOMAIN,REACT_APP_FIREBASE_PROJECT_ID,REACT_APP_FIREBASE_STORAGE_BUCKET,REACT_APP_FIREBASE_MESSAGING_SENDER_ID,REACT_APP_FIREBASE_APP_ID,REACT_APP_RECAPTCHA_SITE_KEY,REACT_APP_GOOGLE_ANALYTICS_ID,REACT_APP_REMOTE_CONFIG_MINIMUM_FETCH_INTERVAL_MILLI,REACT_APP_FEED_API_BASE_URL,REACT_APP_GBFS_VALIDATOR_API_BASE_URL
253257
254258
- name: Run Install for Functions
255259
if: ${{ inputs.DEPLOY_FIREBASE_FUNCTIONS }}

web-app/src/.env.rename_me

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ REACT_APP_RECAPTCHA_SITE_KEY={{REACT_APP_RECAPTCHA_SITE_KEY}}
99
REACT_APP_GOOGLE_ANALYTICS_ID={{REACT_APP_GOOGLE_ANALYTICS_ID}}
1010
REACT_APP_REMOTE_CONFIG_MINIMUM_FETCH_INTERVAL_MILLI={{REACT_APP_REMOTE_CONFIG_MINIMUM_FETCH_INTERVAL_MILLI}}
1111
REACT_APP_FEED_API_BASE_URL={{REACT_APP_FEED_API_BASE_URL}}
12+
REACT_APP_GBFS_VALIDATOR_API_BASE_URL={{REACT_APP_GBFS_VALIDATOR_API_BASE_URL}}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import React, { createContext, useContext, useMemo, useState } from 'react';
2+
import {
3+
type BasicAuth,
4+
type BearerTokenAuth,
5+
type OAuthClientCredentialsGrantAuth,
6+
} from '../store/gbfs-validator-reducer';
7+
8+
export enum AuthTypeEnum {
9+
BASIC = 'BasicAuth',
10+
BEARER = 'BearerTokenAuth',
11+
OAUTH = 'OAuthClientCredentialsGrantAuth',
12+
}
13+
14+
export type GbfsAuthDetails =
15+
| BasicAuth
16+
| BearerTokenAuth
17+
| OAuthClientCredentialsGrantAuth
18+
| undefined;
19+
20+
interface GbfsAuthContextValue {
21+
auth: GbfsAuthDetails;
22+
setAuth: (details: GbfsAuthDetails) => void;
23+
clearAuth: () => void;
24+
}
25+
26+
const GbfsAuthContext = createContext<GbfsAuthContextValue | undefined>(
27+
undefined,
28+
);
29+
30+
const defaultAuth: GbfsAuthDetails = undefined;
31+
32+
export function GbfsAuthProvider({
33+
children,
34+
}: {
35+
children: React.ReactNode;
36+
}): React.ReactElement {
37+
const [auth, setAuthState] = useState<GbfsAuthDetails>(defaultAuth);
38+
39+
const setAuth = (details: GbfsAuthDetails): void => {
40+
setAuthState(details);
41+
};
42+
43+
const clearAuth = (): void => {
44+
setAuthState(defaultAuth);
45+
};
46+
47+
const value = useMemo(() => ({ auth, setAuth, clearAuth }), [auth]);
48+
49+
return (
50+
<GbfsAuthContext.Provider value={value}>
51+
{children}
52+
</GbfsAuthContext.Provider>
53+
);
54+
}
55+
56+
export function useGbfsAuth(): GbfsAuthContextValue {
57+
const ctx = useContext(GbfsAuthContext);
58+
if (ctx == null) {
59+
throw new Error('useGbfsAuth must be used within GbfsAuthProvider');
60+
}
61+
return ctx;
62+
}

web-app/src/app/router/Router.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import GBFSVersionAnalytics from '../screens/Analytics/GBFSVersionAnalytics';
3636
import ContactUs from '../screens/ContactUs';
3737
import FullMapView from '../screens/Feed/components/FullMapView';
3838
import GbfsValidator from '../screens/GbfsValidator';
39+
import { GbfsAuthProvider } from '../context/GbfsAuthProvider';
3940

4041
export const AppRouter: React.FC = () => {
4142
const navigateTo = useNavigate();
@@ -93,7 +94,14 @@ export const AppRouter: React.FC = () => {
9394
<Route path='about' element={<About />} />
9495
<Route path='contact-us' element={<ContactUs />} />
9596
<Route path='feeds' element={<Feeds />} />
96-
<Route path='gbfs-validator' element={<GbfsValidator />} />
97+
<Route
98+
path='gbfs-validator'
99+
element={
100+
<GbfsAuthProvider>
101+
<GbfsValidator />
102+
</GbfsAuthProvider>
103+
}
104+
/>
97105
<Route
98106
path='feeds/gtfs'
99107
element={<Navigate to='/feeds?gtfs=true' replace />}

web-app/src/app/screens/GbfsValidator/GbfsFeedSearchInput.tsx

Lines changed: 115 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -14,75 +14,136 @@ import {
1414
type SelectChangeEvent,
1515
} from '@mui/material';
1616
import SearchIcon from '@mui/icons-material/Search';
17-
import { useState } from 'react';
17+
import { useEffect, useState } from 'react';
1818
import { useNavigate } from 'react-router-dom';
19+
import { AuthTypeEnum, useGbfsAuth } from '../../context/GbfsAuthProvider';
20+
import { useSelector } from 'react-redux';
21+
import { selectGbfsValidationParams } from '../../store/gbfs-validator-selectors';
1922

20-
enum AuthTypeEnum {
21-
BASIC = 'Basic Auth',
22-
BEARER = 'Bearer Token',
23-
OAUTH = 'Oauth Client Credentials Grant',
24-
CUSTOM = 'Custom Headers (e.g. API Key)',
23+
interface GbfsFeedSearchInputProps {
24+
initialFeedUrl?: string;
25+
triggerDataFetch?: () => void;
2526
}
2627

27-
export default function GbfsFeedSearchInput(): React.ReactElement {
28+
export default function GbfsFeedSearchInput({
29+
initialFeedUrl,
30+
triggerDataFetch,
31+
}: GbfsFeedSearchInputProps): React.ReactElement {
32+
const lastSearchParams = useSelector(selectGbfsValidationParams);
2833
const theme = useTheme();
2934
const navigate = useNavigate();
30-
const [autoDiscoveryUrlInput, setAutoDiscoveryUrlInput] =
31-
useState<string>('');
35+
const { auth, setAuth } = useGbfsAuth();
36+
const [autoDiscoveryUrlInput, setAutoDiscoveryUrlInput] = useState<string>(
37+
initialFeedUrl ?? '',
38+
);
3239
const [requiresAuth, setRequiresAuth] = useState(false);
3340
const [authType, setAuthType] = useState<string>('');
34-
const [basicAuthUsername, setBasicAuthUsername] = useState<string>('');
35-
const [basicAuthPassword, setBasicAuthPassword] = useState<string>('');
36-
const [bearerAuthValue, setBearerAuthValue] = useState<string>('');
37-
const [oauthClientId, setOauthClientId] = useState<string>('');
38-
const [oauthClientSecret, setOauthClientSecret] = useState<string>('');
39-
const [oauthTokenUrl, setOauthTokenUrl] = useState<string>('');
41+
const [basicAuthUsername, setBasicAuthUsername] = useState<
42+
string | undefined
43+
>(undefined);
44+
const [basicAuthPassword, setBasicAuthPassword] = useState<
45+
string | undefined
46+
>(undefined);
47+
const [bearerAuthValue, setBearerAuthValue] = useState<string | undefined>(
48+
undefined,
49+
);
50+
const [oauthClientId, setOauthClientId] = useState<string | undefined>(
51+
undefined,
52+
);
53+
const [oauthClientSecret, setOauthClientSecret] = useState<
54+
string | undefined
55+
>(undefined);
56+
const [oauthTokenUrl, setOauthTokenUrl] = useState<string | undefined>(
57+
undefined,
58+
);
59+
60+
// Used to keep the text input up to date with back navigation in browser
61+
useEffect(() => {
62+
setAutoDiscoveryUrlInput(initialFeedUrl ?? '');
63+
}, [initialFeedUrl]);
64+
65+
// Used to keep the auth inputs up to date
66+
useEffect(() => {
67+
setRequiresAuth(auth !== undefined);
68+
setAuthType(auth == undefined ? '' : auth.authType ?? '');
69+
setBasicAuthUsername(
70+
auth != null && 'username' in auth ? auth.username : undefined,
71+
);
72+
setBasicAuthPassword(
73+
auth != null && 'password' in auth ? auth.password : undefined,
74+
);
75+
setBearerAuthValue(
76+
auth != null && 'token' in auth ? auth.token : undefined,
77+
);
78+
setOauthClientId(
79+
auth != null && 'clientId' in auth ? auth.clientId : undefined,
80+
);
81+
setOauthClientSecret(
82+
auth != null && 'clientSecret' in auth ? auth.clientSecret : undefined,
83+
);
84+
setOauthTokenUrl(
85+
auth != null && 'tokenUrl' in auth ? auth.tokenUrl : undefined,
86+
);
87+
}, [auth]);
4088

4189
const handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
4290
setRequiresAuth(event.target.checked);
4391
};
4492

4593
const handleAuthTypeChange = (event: SelectChangeEvent<string>): void => {
4694
setAuthType(event.target.value);
47-
setBasicAuthUsername('');
48-
setBasicAuthPassword('');
49-
setBearerAuthValue('');
50-
setOauthClientId('');
51-
setOauthClientSecret('');
52-
setOauthTokenUrl('');
95+
setBasicAuthUsername(undefined);
96+
setBasicAuthPassword(undefined);
97+
setBearerAuthValue(undefined);
98+
setOauthClientId(undefined);
99+
setOauthClientSecret(undefined);
100+
setOauthTokenUrl(undefined);
53101
};
54102

55103
const isSubmitBoxDisabled = (): boolean => {
56104
if (autoDiscoveryUrlInput === '') return true;
57-
if (requiresAuth) {
58-
if (authType === '') return true;
59-
if (authType === AuthTypeEnum.BASIC) {
60-
if (basicAuthUsername === '' || basicAuthPassword === '') return true;
61-
}
62-
if (authType === AuthTypeEnum.BEARER) {
63-
if (bearerAuthValue === '') return true;
64-
}
65-
if (authType === AuthTypeEnum.OAUTH) {
66-
if (
67-
oauthClientId === '' ||
68-
oauthClientSecret === '' ||
69-
oauthTokenUrl === ''
70-
)
71-
return true;
72-
}
73-
}
105+
if (requiresAuth && authType === '') return true;
74106
return false;
75107
};
76108

77109
const validateGBFSFeed = (): void => {
78-
// 1. dispatch action with url and auth details (state -> loading)
79-
// once done then
80-
// 2. navigate to /gbfs-validator?AutoDiscoveryUrl=url
81-
// or
82-
// navigate to /gbfs-validator?AutoDiscoveryUrl=url&auth details
83-
// store the auth details in context
84-
// let the GbfsValidator component handle the loading state
85-
// I'm sure if a query param exists, instead of navigation, we will update the param, and have a useEffect to call the new feed to validate
110+
if (requiresAuth) {
111+
switch (authType) {
112+
case AuthTypeEnum.BASIC:
113+
setAuth({
114+
authType: AuthTypeEnum.BASIC,
115+
username: basicAuthUsername,
116+
password: basicAuthPassword,
117+
});
118+
break;
119+
case AuthTypeEnum.BEARER:
120+
setAuth({ authType: AuthTypeEnum.BEARER, token: bearerAuthValue });
121+
break;
122+
case AuthTypeEnum.OAUTH:
123+
setAuth({
124+
authType: AuthTypeEnum.OAUTH,
125+
clientId: oauthClientId,
126+
clientSecret: oauthClientSecret,
127+
tokenUrl: oauthTokenUrl,
128+
});
129+
break;
130+
default:
131+
setAuth(undefined);
132+
}
133+
} else {
134+
setAuth(undefined);
135+
}
136+
137+
// If the URL is the same React will ignore navigation, so we trigger the data fetch manually when it's the same url
138+
// Auth change will also trigger a fetch via useEffect in ValidationState
139+
if (
140+
!requiresAuth &&
141+
lastSearchParams?.feedUrl === autoDiscoveryUrlInput &&
142+
triggerDataFetch != undefined
143+
) {
144+
triggerDataFetch();
145+
return;
146+
}
86147
navigate(
87148
`/gbfs-validator?AutoDiscoveryUrl=${encodeURIComponent(
88149
autoDiscoveryUrlInput,
@@ -116,9 +177,10 @@ export default function GbfsFeedSearchInput(): React.ReactElement {
116177
variant='outlined'
117178
label='GBFS Auto-Discovery URL'
118179
placeholder='eg: https://example.com/gbfs.json'
180+
value={autoDiscoveryUrlInput ?? ''}
119181
sx={{ width: '100%', mr: 2 }}
120182
onChange={(e) => {
121-
setAutoDiscoveryUrlInput(e.target.value);
183+
setAutoDiscoveryUrlInput(e.target.value.trim());
122184
}}
123185
InputProps={{
124186
startAdornment: <SearchIcon sx={{ mr: 1 }}></SearchIcon>,
@@ -178,6 +240,7 @@ export default function GbfsFeedSearchInput(): React.ReactElement {
178240
variant='outlined'
179241
label='Username'
180242
placeholder='Enter Username'
243+
value={basicAuthUsername ?? ''}
181244
fullWidth
182245
onChange={(e) => {
183246
setBasicAuthUsername(e.target.value);
@@ -188,6 +251,7 @@ export default function GbfsFeedSearchInput(): React.ReactElement {
188251
variant='outlined'
189252
label='Password'
190253
placeholder='Enter Password'
254+
value={basicAuthPassword ?? ''}
191255
type='password'
192256
fullWidth
193257
onChange={(e) => {
@@ -203,6 +267,7 @@ export default function GbfsFeedSearchInput(): React.ReactElement {
203267
variant='outlined'
204268
label='Token'
205269
placeholder='Enter Bearer Token'
270+
value={bearerAuthValue ?? ''}
206271
sx={{ mt: 2 }}
207272
fullWidth
208273
onChange={(e) => {
@@ -224,6 +289,7 @@ export default function GbfsFeedSearchInput(): React.ReactElement {
224289
size='small'
225290
variant='outlined'
226291
placeholder='Client Id'
292+
value={oauthClientId ?? ''}
227293
label='Client Id'
228294
fullWidth
229295
onChange={(e) => {
@@ -236,6 +302,7 @@ export default function GbfsFeedSearchInput(): React.ReactElement {
236302
placeholder='Enter Client Secret'
237303
label='Client Secret'
238304
fullWidth
305+
value={oauthClientSecret ?? ''}
239306
onChange={(e) => {
240307
setOauthClientSecret(e.target.value);
241308
}}
@@ -246,6 +313,7 @@ export default function GbfsFeedSearchInput(): React.ReactElement {
246313
placeholder='Enter Token Url'
247314
label='Token Url'
248315
fullWidth
316+
value={oauthTokenUrl ?? ''}
249317
onChange={(e) => {
250318
setOauthTokenUrl(e.target.value);
251319
}}

0 commit comments

Comments
 (0)