Commit c6bc432a authored by Viacheslav Pavlov's avatar Viacheslav Pavlov Committed by Matija Obreza
Browse files

ExpressJS and SSR

- Added serverStorage
- Added fetching of access_token at server side
- Oauth token check
- Fixed missing authorities on page reload
- Fixed clearing of cookies after logout, removed jwtSigningKey
parent 0f8ce986
......@@ -47,7 +47,7 @@ if (__PRELOADED_STATE__ === undefined) {
document.getElementsByTagName('html')[0].setAttribute('lang', detectedLang);
document.getElementsByTagName('html')[0].setAttribute('dir', direction);
checkAccessTokens(store.dispatch)
checkAccessTokens(store.dispatch, store.getState)
.then(() => {
// no SSR
ReactDOM.render(
......
......@@ -20,6 +20,7 @@ const config = {
clientId: argv['client-id'] || 'defaultclient@localhost',
clientSecret: argv['client-secret'] || 'changeme',
googleClientId: argv['google-client-id'] || '',
access_token: null,
};
if (config.frontendPath.endsWith('/')) {
......
import * as Loadable from 'react-loadable';
import { configureBackendApi } from 'utilities/requestUtils';
import { LoginService } from 'service/LoginService';
import config from './config';
import { refreshServerStorage } from './middleware/serverStorage';
/**
* Start the frontend server
......@@ -11,6 +14,22 @@ const PORT = process.env.PORT || 3000;
// Configure axios for server
configureBackendApi({ apiUrl: `http://localhost:${PORT}/proxy` });
const refreshAppToken = () => {
LoginService.loginApp(config.clientId).then((token) => {
console.log('Received token: \n', token);
configureBackendApi({ accessToken: token.access_token });
const refreshIn = (token.expires_in - 30) * 1000;
console.log(`Scheduling app token refresh in ${refreshIn}s`);
config.access_token = token;
setTimeout(refreshAppToken, refreshIn);
refreshServerStorage();
}).catch((axiosError) => {
console.log(`Error refreshing app token: ${axiosError.response.statusText}`, axiosError.response);
});
};
refreshAppToken();
// start server on PORT
Loadable.preloadAll().then(() => {
server.listen(PORT, () => {
......
import * as jwt from 'jsonwebtoken';
import { loginAppRequest } from 'actions/login';
import { checkTokenRequest, loginUser } from 'user/actions/public';
import {loginUser} from 'user/actions/public';
import ApiError from 'model/ApiError';
import config from '../config';
function checkAuthTokenRequest(req, dispatch) {
const token = req.cookies.access_token;
const token = req.cookies.access_token || config.access_token.access_token;
if (token) {
console.log('Checking cookie token', token);
return dispatch(checkTokenRequest(token))
.then((checkedToken) => {
console.log(`User token ${token} is valid, response`, checkedToken);
return dispatch(loginUser(checkedToken));
})
.catch(() => {
console.log('Server: check failed dispatching loginAppRequest');
return dispatch(loginAppRequest());
});
try {
const parsedToken = jwt.decode(token);
console.log('Parsed token data', parsedToken);
dispatch(loginUser({access_token: token, ...parsedToken}));
return Promise.resolve({access_token: token, ...parsedToken});
} catch (e) {
console.log('Error while parsing token:', e.message);
return dispatch(loginAppRequest());
}
} else {
console.log('Server: No token in cookie, dispatching loginAppRequest');
return dispatch(loginAppRequest());
......@@ -28,7 +31,8 @@ export default function checkAuthToken(req, res, dispatch) {
console.log(`Setting cookie to expire in ${(data.exp || data.expires_in) / 60}min from`, data);
res.cookie('access_token', data.access_token, { path: '/', expires: new Date(data.exp * 1000 || new Date().getTime() + ((data.expires_in * 1000) || (/* 1hr */ 1000 * 60 * 60))) });
if (data.authorities) {
res.cookie('authorities', JSON.stringify(data.authorities), { path: '/', expires: new Date(data.exp * 1000 || new Date().getTime() + ((data.expires_in * 1000) || (/* 1hr */ 1000 * 60 * 60))) });
console.log(`Setting authorities cookies: `, data.authorities);
res.cookie('authorities', JSON.stringify(data.authorities), { path: '/', expires: new Date(data.exp * 1000 || new Date().getTime() + ((data.expires_in * 1000) || (/* 1hr */ 1000 * 60 * 60))) });
} else {
res.clearCookie('authorities');
}
......
......@@ -37,6 +37,8 @@ import ApiError from 'model/ApiError';
// react-loadable webpack stats
import * as path from 'path';
import {readFileSync} from 'fs';
import { serverStorage } from './serverStorage';
import { receiveServerStorage } from 'actions/serverStorage';
const reactLoadableStats = JSON.parse(readFileSync(path.join('./', 'react-loadable.json'), {encoding: 'utf8'}));
// console.log(`react-loadable stats: `, reactLoadableStats);
......@@ -49,6 +51,7 @@ const prerenderer = (html, errHtml) => (req, res) => {
const store = createStore(rootReducer, {} as any, applyMiddleware(thunk, routerMiddleware(createMemoryHistory)));
// Only send public data to client
store.dispatch(configure({ frontendPath: config.frontendPath, apiUrl: config.apiUrl, googleClientId: config.googleClientId, clientId: config.clientId }));
receiveServerStorage(serverStorage, store.dispatch);
console.log(`Processing request`, req._parsedOriginalUrl);
const pathname = req._parsedOriginalUrl.pathname;
......
import { CropService } from 'service/CropService';
import ApiInfoService from 'service/genesys/ApiInfoService';
const REFRESH_TIME = 15 * 60 * 1000;
export const serverStorage = {
crops: [],
serverInfo: {},
};
const refreshCrops = () => {
CropService.listCrops()
.then((crops) => {
serverStorage.crops = crops;
setTimeout(refreshCrops, REFRESH_TIME);
});
};
const refreshServerInfo = () => {
ApiInfoService.apiInfo()
.then((serverInfo) => {
serverStorage.serverInfo = serverInfo;
setTimeout(refreshServerInfo, REFRESH_TIME);
});
};
export const refreshServerStorage = () => {
refreshCrops();
refreshServerInfo();
};
import { LoginService } from 'service/LoginService';
import * as Constants from 'constants/login';
import * as cookies from 'es-cookie';
import * as jwt from 'jsonwebtoken';
import { clearCookies, saveCookies } from 'utilities';
import { ROLE_CLIENT } from 'constants/userRoles';
import {log} from 'utilities/debug';
import {loginUser} from 'user/actions/public';
import { configureBackendApi } from 'utilities/requestUtils';
export function checkAccessTokens(dispatch) {
export function checkAccessTokens(dispatch, getState) {
const cookieToken: string = typeof window !== 'undefined' && cookies.get('access_token');
console.log('Application config: ', getState().applicationConfig);
const applicationLogin = () =>
LoginService.loginApp()
LoginService.loginApp(getState().applicationConfig.clientId)
.then((data) => {
// console.log('loginApp token', data);
saveCookies({access_token: data.access_token, authorities: [ROLE_CLIENT]}, data.exp * 1000 || new Date().getTime() + data.expires_in * 1000);
......@@ -22,15 +24,15 @@ export function checkAccessTokens(dispatch) {
});
if (cookieToken) {
return LoginService.checkToken(cookieToken)
.then((data) => {
dispatch(loginUser({ access_token: cookieToken, ...data }));
return true;
})
.catch((error) => {
clearCookies();
return applicationLogin();
});
try {
const parsedTokenData = jwt.decode(cookieToken);
dispatch(loginUser({ access_token: cookieToken, ...parsedTokenData }));
return Promise.resolve();
} catch (e) {
console.log('Error while parsing token: ', e.message);
clearCookies();
return applicationLogin();
}
} else {
clearCookies();
return applicationLogin();
......@@ -38,8 +40,8 @@ export function checkAccessTokens(dispatch) {
}
function loginAppRequest() {
return (dispatch) => {
return LoginService.loginApp()
return (dispatch, getState) => {
return LoginService.loginApp(getState().applicationConfig.clientId)
.then((data) => {
return dispatch(loginApp(data));
});
......@@ -49,10 +51,13 @@ function loginAppRequest() {
function loginApp(d) {
// console.log('Login app', d);
configureBackendApi({ accessToken: d.access_token });
return {
return (dispatch, getState) => {
return {
type: Constants.LOGIN_APP,
authorities: [ROLE_CLIENT],
...d,
access_token: d.access_token,
...jwt.decode(d.access_token),
};
};
}
......
import { RECEIVE_CROPS } from '../crop/constants';
import ApiCall from '../model/ApiCall';
import { GET_SERVER_INFO } from '../constants/serverInfo';
// TODO rename
export const receiveServerStorage = (serverStorage, dispatch) => {
console.log('ServerStorage: ', serverStorage);
dispatch({type: RECEIVE_CROPS, payload: {apiCall: ApiCall.success(serverStorage.crops)}});
dispatch({type: GET_SERVER_INFO, payload: {info: serverStorage.serverInfo}});
};
......@@ -3,17 +3,21 @@ import {CONFIGURE_APPLICATION, SET_LANG} from 'constants/applicationConfig';
const INITIAL_STATE: {
frontendUrl: string;
frontendPath: string;
apiUrl: string;
clientId: string;
googleClientId: string;
apiUrl: string;
frontendPath: string;
lang: string,
anonToken: string,
} = {
clientId: process.env.CLIENT_ID || '',
googleClientId: process.env.GOOGLE_CLIENT_ID || '',
apiUrl: process.env.CATALOG_API_URL || 'http://localhost:8080',
frontendUrl: 'http://localhost:3000',
frontendPath: '',
apiUrl: process.env.CATALOG_API_URL || 'http://localhost:8080',
clientId: process.env.CLIENT_ID || 'defaultclient@localhost', // TODO check if '' is required here
googleClientId: process.env.GOOGLE_CLIENT_ID || '',
lang: 'en',
anonToken: null,
};
export default function configure(state = INITIAL_STATE, action: { type?: string, payload?: any } = {type: '', payload: {}}) {
......
......@@ -10,15 +10,17 @@ const URL_RESET_PASSWORD = `/api/v1/me/password/reset`;
const URL_CANCEL_VALIDATION = UrlTemplate.parse(`/api/v1/me/{tokenUuid}/cancel`);
const URL_UPDATE_PASSWORD = UrlTemplate.parse(`/api/v1/me/{tokenUuid}/pwdreset`);
// const LOGOUT_URL = `${API_BASE_URL}/me/logout`;
export const CHECK_TOKEN_URL = `/oauth/check_token`;
export const VERIFY_GOOGLE_TOKEN_URL = `/api/google/verify-token`;
export class LoginService {
public static loginApp() {
public static loginApp(clientId: string, clientSecret?: string) {
return axiosBackend.post(LOGIN_URL, null, {
// baseURL: '/proxy',
params: {
grant_type: 'client_credentials',
client_id: clientId,
client_secret: clientSecret || undefined,
},
})
.then(({ data }) => data);
......@@ -59,17 +61,6 @@ export class LoginService {
}).then(({ data }) => data as any);
}
public static checkToken(token: string) {
return axiosBackend.get(CHECK_TOKEN_URL, {
params: {
token,
},
})
.then(({ data }) => {
return data;
});
}
/**
* @param clientId our OAuth Client ID
* @param tokenId Token returned by Google Auth
......
......@@ -34,11 +34,6 @@ interface IAppProps extends React.ClassAttributes<any> {
class App extends React.Component<IAppProps, any> {
public static needs = [
() => loadCrops(),
() => serverInfoRequest(),
];
public constructor(props: any) {
super(props);
}
......
import * as jwt from 'jsonwebtoken';
// Actions
import {loginApp} from 'actions/login';
import {createPureApiCaller} from 'actions/ApiCall';
import navigateTo from 'actions/navigation';
import {loginAppRequest} from 'actions/login';
// Constants
import * as Constants from 'constants/login';
......@@ -10,13 +11,11 @@ import {LoginService} from 'service/LoginService';
import {UserService} from 'service/UserService';
// Util
import {clearCookies, saveCookies} from 'utilities';
import {log} from 'utilities/debug';
import { configureBackendApi } from 'utilities/requestUtils';
// Wrapped API Calls
const apiRegister = createPureApiCaller(UserService.register);
const apiLogin = createPureApiCaller(LoginService.login);
const apiCheckToken = createPureApiCaller(LoginService.checkToken);
const apiVerifyGoogleToken = createPureApiCaller(LoginService.verifyGoogleToken);
const apiLogout = createPureApiCaller(LoginService.logout);
const apiResetPassword = createPureApiCaller(LoginService.resetPassword);
......@@ -39,46 +38,25 @@ export const loginUser = (payload) => {
};
};
const checkToken = (payload) => {
return {
type: Constants.CHECK_TOKEN,
...payload,
};
};
export const loginRequest = (username, password) => (dispatch) => {
export const loginRequest = (username, password) => (dispatch, getState) => {
return dispatch(apiLogin(username, password))
.then((data) => {
saveCookies(data, data.exp * 1000 || new Date().getTime() + data.expires_in * 1000);
return dispatch(loginUser(data));
});
};
const tokenData = jwt.decode(data.access_token);
saveCookies({access_token: data.access_token, ...tokenData}, data.exp * 1000 || new Date().getTime() + data.expires_in * 1000);
export const checkTokenRequest = (token) => (dispatch) => {
log('checkTokenRequest verifying ', token);
return dispatch(apiCheckToken(token))
.then((data) => {
log('checkTokenRequest got', data);
return dispatch(checkToken({access_token: token, ...data}));
return dispatch(loginUser({...data, ...tokenData}));
});
};
// Google login
// TODO find usage
export const verifyGoogleToken = (payload) => {
return {
type: Constants.VERIFY_GOOGLE_TOKEN,
...payload,
};
};
export const verifyGoogleTokenRequest = (tokenId) => (dispatch, getState) => {
const clientId = getState().applicationConfig.clientId;
return dispatch(apiVerifyGoogleToken(clientId, tokenId))
.then((data) => {
saveCookies(data, data.exp * 1000 || new Date().getTime() + data.expires_in * 1000);
return dispatch(loginApp(data));
const tokenData = jwt.decode(data.access_token);
saveCookies({access_token: data.access_token, ...tokenData}, data.exp * 1000 || new Date().getTime() + data.expires_in * 1000);
return dispatch(loginUser({...data, ...tokenData}));
});
};
......@@ -92,8 +70,10 @@ const logout = () => {
export const logoutRequest = () => (dispatch, getState) => {
const token = getState().login.access_token;
return dispatch(apiLogout(token))
.then(() => {
.then((token) => {
clearCookies();
dispatch(loginAppRequest())
.then((data) => saveCookies(data, data.exp * 1000 || new Date().getTime() + data.expires_in * 1000));
return dispatch(logout());
});
};
......@@ -113,10 +93,6 @@ export const cancelValidation = (tokenUuid: string, gRecaptchaResponse?: string)
export const loginAction = (username, password, needRedirect = false): any => (dispatch) => {
// log('Trying login', username);
return dispatch(loginRequest(username, password))
.then(({access_token}) => {
// log('Access token', access_token);
return dispatch(checkTokenRequest(access_token));
})
.then((data) => {
if (needRedirect) {
dispatch(navigateTo('/'));
......@@ -130,10 +106,6 @@ export const loginAction = (username, password, needRedirect = false): any => (d
export const googleLogin = (response, needRedirect): any => (dispatch) => {
// log('Trying google login');
return dispatch(verifyGoogleTokenRequest(response.tokenId))
.then(({access_token}) => {
// log('Access token', access_token);
return dispatch(checkTokenRequest(access_token));
})
.then((data) => {
if (needRedirect) {
dispatch(navigateTo('/'));
......
......@@ -35,15 +35,20 @@ axiosBackend.interceptors.request.use((config) => {
* @param apiUrl baseURL to the backend API (http://localhost:3000/proxy)
* @param authToken OAuth authorization token
*/
export const configureBackendApi = ({apiUrl, accessToken, timeout = 0}: {apiUrl?: string, accessToken?: string, timeout?: number}) => {
export const configureBackendApi = ({apiUrl, accessToken, origin, timeout = 20000}: {apiUrl?: string, accessToken?: string, origin?: string, timeout?: number}) => {
if (apiUrl) {
console.log(`Using backend API baseURL ${apiUrl}`);
axiosBackend.defaults.baseURL = apiUrl;
console.log(`axiosBackend.defaults.headers`, axiosBackend.defaults.headers);
}
if (accessToken) {
console.log(`Using backend API accessToken ..snip..`);
axiosBackend.defaults.headers.common.Authorization = `Bearer ${accessToken}`;
}
if (origin) {
console.log(`Using origin ${origin}`);
axiosBackend.defaults.headers.common.Origin = origin;
}
console.log(`Using backend API timeout ${timeout}`);
axiosBackend.defaults.timeout = timeout;
};
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment