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 15 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
32 changes: 25 additions & 7 deletions react/src/redux/api/geoapi.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
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';
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
// and if user is logged in
nathanfranklin marked this conversation as resolved.
Show resolved Hide resolved

// 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 +30,18 @@ export const geoapi = createApi({
},
}),
tagTypes: ['Test'],
endpoints: () => ({}),
endpoints: (builder) => ({
getGeoapiProjects: builder.query<any, void>({
query: () => '/projects/',
}),
// NOTE: Currently fails due to cors on localhost (chrome) works when requesting production backend
getGeoapiUserInfo: builder.query<any, void>({
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>) => {
const u: any = {
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) {
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);
}