Skip to content

Commit

Permalink
Task/WG-10: Add projects and user queries. (#137)
Browse files Browse the repository at this point in the history
* Add login/logout and protected routes

* Refactor

* Fix tests

* Fix linting

* Fix linting

* Add projects and user queries.

* Fix bug.

* Fix test.

* Add prettier.

* Update authUtils.ts

* Update react/src/redux/api/geoapi.ts

* Add link to related jira issue

* Fix prettier issue

---------

Co-authored-by: Nathan Franklin <[email protected]>
  • Loading branch information
duckonomy and nathanfranklin authored Dec 15, 2023
1 parent 8eb641e commit fbe4f81
Show file tree
Hide file tree
Showing 13 changed files with 130 additions and 37 deletions.
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",
"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';

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 @@ export const geoapi = createApi({
},
}),
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 @@ const authSlice = createSlice({
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);
}

0 comments on commit fbe4f81

Please sign in to comment.