Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: authz'd scoped requests for Katsu public endpoints #173

Closed
wants to merge 8 commits into from
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ on:
pull_request:
branches:
- main
- features/**
- feat/**
push:
branches:
- main
Expand Down
505 changes: 258 additions & 247 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
"@ant-design/icons": "^5.3.1",
"@reduxjs/toolkit": "^1.9.7",
"antd": "^5.15.0",
"axios": "^1.6.2",
"bento-auth-js": "^5.1.1",
"axios": "^1.7.3",
"bento-auth-js": "^6.0.1",
"bento-charts": "^2.6.8",
"dotenv": "^16.3.1",
"i18next": "^23.7.7",
Expand Down
2 changes: 1 addition & 1 deletion src/js/components/BentoAppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const BentoAppRouter = () => {
dispatch(makeGetProvenanceRequest());
dispatch(makeGetKatsuPublic());
dispatch(fetchKatsuData());
}, [selectedScope]);
}, [isAuthenticated, selectedScope]);

useEffect(() => {
dispatch(getProjects());
Expand Down
6 changes: 3 additions & 3 deletions src/js/components/SiteHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useNavigate, useLocation } from 'react-router-dom';

import { Button, Flex, Layout, Typography, Space } from 'antd';
import { useTranslation } from 'react-i18next';
import { useIsAuthenticated, usePerformAuth, usePerformSignOut } from 'bento-auth-js';
import { useAuthState, useIsAuthenticated, useOpenIdConfig, usePerformAuth, usePerformSignOut } from 'bento-auth-js';

import { RiTranslate } from 'react-icons/ri';
import { ExportOutlined, LinkOutlined, LoginOutlined, LogoutOutlined, ProfileOutlined } from '@ant-design/icons';
Expand All @@ -25,8 +25,8 @@ const SiteHeader = () => {
const navigate = useNavigate();
const location = useLocation();

const { isFetching: openIdConfigFetching } = useAppSelector((state) => state.openIdConfiguration);
const { isHandingOffCodeForToken } = useAppSelector((state) => state.auth);
const { isFetching: openIdConfigFetching } = useOpenIdConfig();
const { isHandingOffCodeForToken } = useAuthState();
const { projects, selectedScope } = useAppSelector((state) => state.metadata);

const scopeProps = {
Expand Down
4 changes: 2 additions & 2 deletions src/js/constants/configConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ export const MAX_CHARTS = 3;
export const katsuPublicOverviewUrl = `${PORTAL_URL}/api/metadata/api/public_overview`;
export const katsuPublicRulesUrl = `${PORTAL_URL}/api/metadata/api/public_rules`;
export const searchFieldsUrl = `${PORTAL_URL}/api/metadata/api/public_search_fields`;
export const katsuUrl = `${PORTAL_URL}/api/metadata/api/public`;
export const katsuPublicSearchUrl = `${PORTAL_URL}/api/metadata/api/public`;
export const provenanceUrl = `${PORTAL_URL}/api/metadata/api/public_dataset`;
export const projectsUrl = `${PORTAL_URL}/api/metadata/api/projects`;
export const katsuLastIngestionsUrl = '/katsu/data-types';
export const katsuLastIngestionsUrl = `${PORTAL_URL}/api/metadata/data-types`;
export const gohanLastIngestionsUrl = '/gohan/data-types';

export const DEFAULT_TRANSLATION = 'default_translation';
Expand Down
7 changes: 4 additions & 3 deletions src/js/features/config/config.store.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import axios from 'axios';
import { PUBLIC_URL } from '@/config';
import { katsuPublicRulesUrl } from '@/constants/configConstants';
import { printAPIError } from '@/utils/error.util';
import { ServiceInfoStore, ServicesResponse } from '@/types/services';
import { RootState } from '@/store';
import { PUBLIC_URL } from '@/config';
import { DiscoveryRules } from '@/types/configResponse';
import { printAPIError } from '@/utils/error.util';
import { scopedAuthorizedRequestConfig } from '@/utils/requests';

export const makeGetConfigRequest = createAsyncThunk<DiscoveryRules, void, { rejectValue: string; state: RootState }>(
'config/getConfigData',
(_, { rejectWithValue, getState }) => {
return axios
.get(katsuPublicRulesUrl, { params: getState().metadata.selectedScope })
.get(katsuPublicRulesUrl, scopedAuthorizedRequestConfig(getState()))
.then((res) => res.data)
.catch(printAPIError(rejectWithValue));
}
Expand Down
17 changes: 9 additions & 8 deletions src/js/features/data/makeGetDataRequest.thunk.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
import { MAX_CHARTS, katsuPublicOverviewUrl } from '@/constants/configConstants';

import { verifyData, saveValue, getValue, convertSequenceAndDisplayData } from '@/utils/localStorage';

import { MAX_CHARTS, katsuPublicOverviewUrl } from '@/constants/configConstants';
import { DEFAULT_CHART_WIDTH, LOCALSTORAGE_CHARTS_KEY } from '@/constants/overviewConstants';
import { serializeChartData } from '@/utils/chart';
import { ChartConfig } from '@/types/chartConfig';
import { ChartDataField, LocalStorageData, Sections } from '@/types/data';
import { Counts, OverviewResponse } from '@/types/overviewResponse';
import { printAPIError } from '@/utils/error.util';
import { RootState } from '@/store';
import { verifyData, saveValue, getValue, convertSequenceAndDisplayData } from '@/utils/localStorage';
import { scopedAuthorizedRequestConfig } from '@/utils/requests';

import type { RootState } from '@/store';
import type { ChartConfig } from '@/types/chartConfig';
import type { ChartDataField, LocalStorageData, Sections } from '@/types/data';
import type { Counts, OverviewResponse } from '@/types/overviewResponse';

export const makeGetDataRequestThunk = createAsyncThunk<
{ sectionData: Sections; counts: Counts; defaultData: Sections },
void,
{ rejectValue: string; state: RootState }
>('data/makeGetDataRequest', async (_, { rejectWithValue, getState }) => {
const overviewResponse = (await axios
.get(katsuPublicOverviewUrl, { params: getState().metadata.selectedScope })
.get(katsuPublicOverviewUrl, scopedAuthorizedRequestConfig(getState()))
.then((res) => res.data)
.catch(printAPIError(rejectWithValue))) as OverviewResponse['overview'];

Expand Down
21 changes: 15 additions & 6 deletions src/js/features/dataTypes/dataTypes.store.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import { PayloadAction, createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
import axios from 'axios';

import type { RootState } from '@/store';
import { authorizedRequestConfig } from '@/utils/requests';

// TODO: find a way to allow this without an auth token
export const makeGetDataTypes = createAsyncThunk('dataTypes/makeGetDataTypes', async () => {
const res = await axios.get('/api/service-registry/data-types');
const data = res.data;
return data;
export const makeGetDataTypes = createAsyncThunk<
object,
void,
{
rejectValue: string;
state: RootState;
}
>('dataTypes/makeGetDataTypes', async (_, { getState }) => {
const res = await axios.get('/api/service-registry/data-types', authorizedRequestConfig(getState()));
return res.data;
});

export type DataTypesState = {
Expand All @@ -26,7 +35,7 @@ const dataTypes = createSlice({
builder.addCase(makeGetDataTypes.pending, (state) => {
state.isFetching = true;
});
builder.addCase(makeGetDataTypes.fulfilled, (state, { payload }: PayloadAction<{ en: string; fr: string }>) => {
builder.addCase(makeGetDataTypes.fulfilled, (state, { payload }: PayloadAction<object>) => {
state.isFetching = false;
state.dataTypes = { ...payload };
});
Expand Down
16 changes: 12 additions & 4 deletions src/js/features/ingestion/lastIngestion.store.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import axios from 'axios';
import { katsuLastIngestionsUrl, gohanLastIngestionsUrl } from '@/constants/configConstants';
import { printAPIError } from '@/utils/error.util';

import type { RootState } from '@/store';
import { LastIngestionDataTypeResponse, DataTypeMap } from '@/types/lastIngestionDataTypeResponse';
import { printAPIError } from '@/utils/error.util';
import { authorizedRequestConfig } from '@/utils/requests';

// Async thunks to fetch data from the two endpoints
export const fetchKatsuData = createAsyncThunk('dataTypes/fetchKatsuData', (_, { rejectWithValue }) =>
export const fetchKatsuData = createAsyncThunk<
LastIngestionDataTypeResponse[],
void,
{
rejectValue: string;
state: RootState;
}
>('dataTypes/fetchKatsuData', (_, { rejectWithValue, getState }) =>
axios
.get(katsuLastIngestionsUrl)
.get(katsuLastIngestionsUrl, authorizedRequestConfig(getState()))
.then((res) => res.data)
.catch(printAPIError(rejectWithValue))
);
Expand Down
20 changes: 13 additions & 7 deletions src/js/features/search/makeGetKatsuPublic.thunk.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
import { katsuUrl } from '@/constants/configConstants';
import { KatsuSearchResponse } from '@/types/search';
import { katsuPublicSearchUrl } from '@/constants/configConstants';
import type { RootState } from '@/store';
import type { KatsuSearchResponse } from '@/types/search';
import { printAPIError } from '@/utils/error.util';
import { RootState } from '@/store';
import { authorizedRequestConfig } from '@/utils/requests';

export const makeGetKatsuPublic = createAsyncThunk<
KatsuSearchResponse,
void,
{ state: RootState; rejectValue: string }
>('query/makeGetKatsuPublic', (_ignore, thunkAPI) => {
const queryParams = thunkAPI.getState().query.queryParams;
>('query/makeGetKatsuPublic', (_, { rejectWithValue, getState }) => {
const state = getState();
const scopeParams = state.metadata.selectedScope;
const queryParams = { ...scopeParams, ...state.query.queryParams };

return axios
.get(katsuUrl, { params: queryParams })
.get(katsuPublicSearchUrl, {
...authorizedRequestConfig(state),
params: queryParams,
})
.then((res) => res.data)
.catch(printAPIError(thunkAPI.rejectWithValue));
.catch(printAPIError(rejectWithValue));
});
6 changes: 5 additions & 1 deletion src/js/features/search/makeGetSearchFields.thunk.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
import { makeAuthorizationHeader } from 'bento-auth-js';
import { searchFieldsUrl } from '@/constants/configConstants';
import { printAPIError } from '@/utils/error.util';
import { SearchFieldResponse } from '@/types/search';
Expand All @@ -11,7 +12,10 @@ export const makeGetSearchFields = createAsyncThunk<
{ rejectValue: string; state: RootState }
>('query/makeGetSearchFields', async (_, { rejectWithValue, getState }) => {
return await axios
.get(searchFieldsUrl, { params: getState().metadata.selectedScope })
.get(searchFieldsUrl, {
headers: { ...makeAuthorizationHeader(getState().auth.accessToken) },
params: getState().metadata.selectedScope,
})
.then((res) => res.data)
.catch(printAPIError(rejectWithValue));
});
12 changes: 12 additions & 0 deletions src/js/utils/requests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { AxiosRequestConfig } from 'axios';
import { makeAuthorizationHeader } from 'bento-auth-js';
import type { RootState } from '@/store';

export const authorizedRequestConfig = (state: RootState): AxiosRequestConfig => ({
headers: { ...makeAuthorizationHeader(state.auth.accessToken) },
});

export const scopedAuthorizedRequestConfig = (state: RootState): AxiosRequestConfig => ({
...authorizedRequestConfig(state),
params: state.metadata.selectedScope,
});