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

Task/WG-10: Add projects and user queries. #137

Merged
merged 20 commits into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
"test": "jest",
"lint": "npm run lint:js",
"lint:js": "eslint . --ext .js,.jsx,.ts,.tsx",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix"
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
"prettier:check": "prettier --single-quote --check src",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this need to be added to CI?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"prettier:fix": "prettier --single-quote --write src"
},
"eslintConfig": {
"extends": "react-app"
Expand Down
2 changes: 1 addition & 1 deletion react/src/components/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({

function AppRouter() {
const isAuthenticated = useSelector((state: RootState) =>
isTokenValid(state.auth)
isTokenValid(state.auth.token)
);

return (
Expand Down
2 changes: 1 addition & 1 deletion react/src/components/Authentication/Login/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ function Login() {
const location = useLocation();
const navigate = useNavigate();
const isAuthenticated = useSelector((state: RootState) =>
isTokenValid(state.auth)
isTokenValid(state.auth.token)
);

useEffect(() => {
Expand Down
8 changes: 7 additions & 1 deletion react/src/components/MainMenu/MainMenu.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import React from 'react';
import { render } from '@testing-library/react';
import MainMenu from './MainMenu';
import { Provider } from 'react-redux';
import store from '../../redux/store';

test('renders menu', () => {
const { getByText } = render(<MainMenu />);
const { getByText } = render(
<Provider store={store}>
<MainMenu />
</Provider>
);
expect(getByText(/Main Menu/)).toBeDefined();
});
6 changes: 6 additions & 0 deletions react/src/components/MainMenu/MainMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import React from 'react';
import {
useGetGeoapiProjectsQuery,
useGetGeoapiUserInfoQuery,
} from '../../redux/api/geoapi';

function MainMenu() {
useGetGeoapiProjectsQuery();
useGetGeoapiUserInfoQuery();
return <h2>Main Menu</h2>;
}

Expand Down
29 changes: 22 additions & 7 deletions react/src/redux/api/geoapi.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/dist/query/react';
import store from '../store';
import type { RootState } from '../store';

// TODO: make configurable so can be https://agave.designsafe-ci.org/geo-staging/v2 or https://agave.designsafe-ci.org/geo/v2
const BASE_URL = 'https:localhost:8888';
// See https://tacc-main.atlassian.net/browse/WG-196
const BASE_URL = 'https://agave.designsafe-ci.org/geo/v2';
nathanfranklin marked this conversation as resolved.
Show resolved Hide resolved

export const geoapi = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: BASE_URL,
prepareHeaders: (headers) => {
prepareHeaders: (headers, api) => {
// TODO check if logged in as we don't want to add if public
const token = store.getState().auth.token;
const token = (api.getState() as RootState).auth.token;

if (token) {
headers.set('Authorization', `Bearer ${token}`);
headers.set('Authorization', `Bearer ${token.token}`);
}

headers.set('Content-Type', 'application/json;charset=UTF-8');
headers.set('Authorization', 'anonymous');

// TODO below adding of JWT if localhost and then add JWT
// we put the JWT on the request to our geoapi API because it is not behind ws02 if in local dev
Expand All @@ -25,5 +27,18 @@
},
}),
tagTypes: ['Test'],
endpoints: () => ({}),
endpoints: (builder) => ({
getGeoapiProjects: builder.query<any, void>({

Check warning on line 31 in react/src/redux/api/geoapi.ts

View workflow job for this annotation

GitHub Actions / React-Linting

Unexpected any. Specify a different type
query: () => '/projects/',
}),
// NOTE: Currently fails due to cors on localhost (chrome) works when requesting production backend
getGeoapiUserInfo: builder.query<any, void>({

Check warning on line 35 in react/src/redux/api/geoapi.ts

View workflow job for this annotation

GitHub Actions / React-Linting

Unexpected any. Specify a different type
query: () => ({
url: 'https://agave.designsafe-ci.org/oauth2/userinfo?schema=openid',
method: 'GET',
}),
}),
}),
});

export const { useGetGeoapiProjectsQuery, useGetGeoapiUserInfoQuery } = geoapi;
48 changes: 33 additions & 15 deletions react/src/redux/authSlice.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import {
getAuthFromLocalStorage,
setAuthToLocalStorage,
removeAuthFromLocalStorage,
getTokenFromLocalStorage,
setTokenToLocalStorage,
removeTokenFromLocalStorage,
} from '../utils/authUtils';
import { AuthState, AuthenticatedUser } from '../types';
import { geoapi } from './api/geoapi';

// TODO consider moving to ../types/
export interface AuthState {
token: string | null;
expires: number | null;
}

// check local storage for our initial state
const initialState: AuthState = getAuthFromLocalStorage();
const initialState: AuthState = {
token: getTokenFromLocalStorage(),
user: null,
};

const authSlice = createSlice({
name: 'auth',
initialState,
Expand All @@ -21,19 +22,36 @@
state,
action: PayloadAction<{ token: string; expires: number }>
) {
state.token = action.payload.token;
state.expires = action.payload.expires;
state.token = {
token: action.payload.token,
expires: action.payload.expires,
};

// save to local storage
setAuthToLocalStorage(state);
setTokenToLocalStorage(state.token);
},
logout(state) {
state.user = null;
state.token = null;
state.expires = null;

//remove from local storage
removeAuthFromLocalStorage();
removeTokenFromLocalStorage();
},

setUser(state, action: PayloadAction<{ user: AuthenticatedUser }>) {
state.user = action.payload.user;
},
},
extraReducers: (builder) => {
builder.addMatcher(
geoapi.endpoints.getGeoapiUserInfo.matchFulfilled,
(state, action: PayloadAction<any>) => {

Check warning on line 47 in react/src/redux/authSlice.ts

View workflow job for this annotation

GitHub Actions / React-Linting

Unexpected any. Specify a different type
const u: any = {

Check warning on line 48 in react/src/redux/authSlice.ts

View workflow job for this annotation

GitHub Actions / React-Linting

Unexpected any. Specify a different type
name: action.payload.name,
email: action.payload.email,
};
state.user = u;
}
);
},
});

Expand Down
18 changes: 18 additions & 0 deletions react/src/redux/projectsSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { createSlice } from '@reduxjs/toolkit';
import { geoapi } from './api/geoapi';

const slice = createSlice({
name: 'projects',
initialState: { projects: [] },
reducers: {},
extraReducers: (builder) => {
builder.addMatcher(
geoapi.endpoints.getGeoapiProjects.matchFulfilled,
(state, { payload }) => {
state.projects = payload;
}
);
},
});

export default slice.reducer;
2 changes: 2 additions & 0 deletions react/src/redux/reducers/reducers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { combineReducers } from 'redux';
import { geoapi } from '../api/geoapi';
import authReducer from '../authSlice';
import projectsReducer from '../projectsSlice';

export const reducer = combineReducers({
auth: authReducer,
projects: projectsReducer,
[geoapi.reducerPath]: geoapi.reducer,
});
7 changes: 7 additions & 0 deletions react/src/redux/store.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { configureStore } from '@reduxjs/toolkit';
import { reducer } from './reducers/reducers';
import { geoapi } from './api/geoapi';

const store = configureStore({
reducer: reducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({ serializableCheck: false }).concat(
geoapi.middleware
),
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

export default store;
14 changes: 14 additions & 0 deletions react/src/types/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export interface AuthenticatedUser {
username: string | null;
email: string | null;
}

export interface AuthToken {
token: string | null;
expires: number | null;
}

export interface AuthState {
user: AuthenticatedUser | null;
token: AuthToken | null;
}
1 change: 1 addition & 0 deletions react/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export type {
FeatureClass,
FeatureCollection,
} from './feature';
export type { AuthState, AuthenticatedUser, AuthToken } from './auth';
26 changes: 15 additions & 11 deletions react/src/utils/authUtils.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,37 @@
import { AuthState } from '../redux/authSlice';
import { AuthToken } from '../types';

export const AUTH_KEY = 'auth';

export function isTokenValid(auth: AuthState): boolean {
if (!auth.expires) {
export function isTokenValid(token: AuthToken | null): boolean {
if (token) {
if (!token.expires) {
return false;
}

const now = Date.now();
return now < token.expires;
} else {
return false;
}

const now = Date.now();
return now < auth.expires;
}

export function getAuthFromLocalStorage(): AuthState {
export function getTokenFromLocalStorage(): AuthToken {
try {
const tokenStr = localStorage.getItem(AUTH_KEY);
if (tokenStr) {
const auth = JSON.parse(tokenStr);
return { token: auth.token, expires: auth.expires };
return auth;
}
} catch (e: any) {

Check warning on line 25 in react/src/utils/authUtils.ts

View workflow job for this annotation

GitHub Actions / React-Linting

Unexpected any. Specify a different type
console.error('Error loading state from localStorage:', e);
}
return { token: null, expires: null };
}

export function setAuthToLocalStorage(auth: AuthState) {
localStorage.setItem(AUTH_KEY, JSON.stringify(auth));
export function setTokenToLocalStorage(token: AuthToken) {
localStorage.setItem(AUTH_KEY, JSON.stringify(token));
}

export function removeAuthFromLocalStorage() {
export function removeTokenFromLocalStorage() {
localStorage.removeItem(AUTH_KEY);
}
Loading