Commit 7e80011d authored by Maxym Borodenko's avatar Maxym Borodenko

#5 Default proxy

parent cbb11fca
......@@ -15,9 +15,9 @@ const PORT = process.env.PORT || 3000;
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
const SSR = process.env.SSR;
const GENESYS_API_URL = process.env.GENESYS_API_URL || 'http://localhost:8080';
const CLIENT_ID = process.env.CLIENT_ID || 'my-trusted-client';
const CLIENT_SECRET = process.env.CLIENT_SECRET || 'my-secret-client';
const API_URL = process.env.API_URL || 'http://localhost:8080';
const CLIENT_ID = process.env.CLIENT_ID || 'defaultclient@localhost';
const CLIENT_SECRET = process.env.CLIENT_SECRET || 'changeme';
const sortedChunks = function(list) {
return function(chunk1, chunk2) {
......@@ -53,41 +53,48 @@ module.exports = {
},
clientLogLevel: "warning",
proxy: {
'/api/genesys': {
target: GENESYS_API_URL,
'/proxy': {
target: API_URL,
logLevel: 'debug',
ws: true,
// secure: false,
pathRewrite(path, req) {
let p = path.replace('/api/genesys', '');
let p = path.replace('/proxy', '');
if (p.startsWith('/oauth/token')) {
const grantType = req.query['grant_type'];
if(grantType === 'client_credentials' || grantType === 'password') {
p = `${p}&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}`;
}
// remove all headers from request
req.headers = {};
} else {
p = path.replace('/api/genesys', '/api');
req.headers = { ...req.headers };
// scrub headers
delete req.headers['host'];
delete req.headers['origin'];
delete req.headers['referer'];
delete req.headers['cookie'];
const grantType = req.query['grant_type'];
if(grantType === 'client_credentials' || grantType === 'password') {
p = `${p}&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}`;
}
// remove all headers from request
req.headers = {};
} else if (p.indexOf('/google/verify-token') === 0) {
p = `${p}&clientId=${CLIENT_ID}`;
}
if (p.startsWith('/uploads')) {
p = p.replace(/^\/uploads/, '/api/v0/repository/download');
}
// If authorization header is not provided, use access_token from cookie
// console.log('Cookies', req.headers.cookie, req);
if (! req.headers.authorization && req.headers.cookie) {
const authorization = req.headers.cookie.match(/access_token=([^;]+)/);
// console.log('Cookies', req.headers.cookie, authorization);
if (authorization && authorization.length === 2) {
console.log('Injecting Authorization header from cookies');
req.headers.authorization = 'Bearer ' + authorization[1];
}
}
console.log(`Proxy headers: `, req.headers);
console.log(`HTTP proxy ${path} to ${GENESYS_API_URL}${p}`);
return p;
},
onError(err, req, res) {
console.log(err);
console.log(err);
},
onClose(res, socket, head) {
// view disconnected websocket connections
console.log('Client disconnected', res);
// view disconnected websocket connections
console.log('Client disconnected', res);
}
}
},
......
import * as proxy from 'express-http-proxy';
import config from '../config';
const genesysProxy = proxy(config.apiUrl, {
parseReqBody: false,
timeout: config.apiTimeout,
filter: (req, res) => {
if (req.url.startsWith('/oauth/') || req.url.startsWith('/token') || req.url.startsWith('/google')) {
// console.log('Will proxy /oauth');
return true;
} else {
console.log(`Will HTTP Proxy filter? ${typeof req.headers.authorization !== 'undefined'}`);
return typeof req.headers.authorization !== 'undefined';
}
},
proxyReqPathResolver: (req) => {
let path = req.url; // .replace(config.frontendPath, '');
if (path.startsWith('/oauth/token')) {
const grantType = req.query.grant_type;
if (grantType === 'client_credentials' || grantType === 'password') {
path = `${path}&client_id=${config.clientId}&client_secret=${config.clientSecret}`;
// remove all headers from request
req.headers = {};
}
} else {
// insert /api
path = `/api${path}`;
// remove some heaaders
delete req.headers.host;
delete req.headers.origin;
delete req.headers.referer;
delete req.headers.cookie;
}
console.log(`HTTP proxy to ${config.apiUrl}${path}`);
return path;
},
});
export default genesysProxy;
import * as proxy from 'express-http-proxy';
import config from '../config';
const httpProxy = proxy(config.apiUrl, {
parseReqBody: false,
timeout: config.apiTimeout,
filter: (req, res) => {
if (req.url.startsWith('/oauth/') || req.url.startsWith('/token') || req.url.startsWith('/google')) {
// console.log('Will proxy /oauth');
return true;
}
if (req.url.startsWith('/uploads')) {
req.url = req.url.replace(/^\/uploads/, '/api/v0/repository/download');
}
if (req.url.startsWith('/api')) {
// If authorization header is not provided, use access_token from cookie
if (!req.headers.authorization && req.headers.cookie) {
const authorization = req.headers.cookie.match(/access_token=([^;]+)/);
// console.log('Cookies', req.headers.cookie, authorization);
if (authorization.length === 2) {
console.log('Injecting Authorization header from cookies');
req.headers.authorization = 'Bearer ' + authorization[1];
}
}
console.log(`Will HTTP Proxy filter? ${typeof req.headers.authorization !== 'undefined'}`);
return typeof req.headers.authorization !== 'undefined';
}
// Not proxying the request
console.log(`Not proxying ${req.url}`);
return false;
},
proxyReqPathResolver: (req) => {
let path = req.url;
if (path.startsWith('/oauth/token')) {
const grantType = req.query.grant_type;
if (grantType === 'client_credentials' || grantType === 'password') {
path = `${path}&client_id=${config.clientId}&client_secret=${config.clientSecret}`;
}
} else if (path.startsWith('/google/verify-token')) {
path = `${path}&clientId=${config.clientId}`;
}
console.log(`HTTP proxy to ${config.apiUrl}${path}`);
return path;
},
});
export default httpProxy;
......@@ -25,6 +25,7 @@ import fetchComponentData from './fetchComponentData';
import detectLocaleFromPath from './detectLocaleFromPath';
import getDir from './detectDirection';
import config from '../config';
import checkAuthToken from './checkAuthToken';
const prerenderer = (html) => (req, res) => {
console.log('Init prerenderer, request url:', req.url);
......@@ -101,41 +102,41 @@ const prerenderer = (html) => (req, res) => {
setLocale();
// checkAuthToken(req, res, store.dispatch).then(() => {
const language = req.i18n.language;
const pathWithoutLang = pathname.substr(language !== 'en' ? 3 : 0, pathname.length);
console.log(`Rendering ${pathWithoutLang} for ${pathname}`);
const branch = matchRoutes(routes, pathWithoutLang);
fetchComponentData(store.dispatch, branch, search)
.then(() => { return renderView();
}).then((html) => {
const serverRenderTime = `${Date.now() - startTime}ms`;
console.log('Server render time:', startTime, Date.now(), serverRenderTime);
const r = html.replace('__SERVER_RENDER_TIME__', serverRenderTime);
const keyStatus = 'status';
const keyUrl = 'url';
const status = context[keyStatus];
const url = context[keyUrl];
if (status && status === 404) {
res.status(404);
}
if (url) {
if (url === '/login') {
res.status(401);
} else {
res.status(302);
checkAuthToken(req, res, store.dispatch).then(() => {
const language = req.i18n.language;
const pathWithoutLang = pathname.substr(language !== 'en' ? 3 : 0, pathname.length);
console.log(`Rendering ${pathWithoutLang} for ${pathname}`);
const branch = matchRoutes(routes, pathWithoutLang);
fetchComponentData(store.dispatch, branch, search)
.then(() => { return renderView();
}).then((html) => {
const serverRenderTime = `${Date.now() - startTime}ms`;
console.log('Server render time:', startTime, Date.now(), serverRenderTime);
const r = html.replace('__SERVER_RENDER_TIME__', serverRenderTime);
const keyStatus = 'status';
const keyUrl = 'url';
const status = context[keyStatus];
const url = context[keyUrl];
if (status && status === 404) {
res.status(404);
}
if (url) {
if (url === '/login') {
res.status(401);
} else {
res.status(302);
}
}
}
return res.set('Content-Type', 'text/html').send(r);
}).catch((err) => {
console.error('Error:', err);
res.status(500).end(err.message);
return res.set('Content-Type', 'text/html').send(r);
}).catch((err) => {
console.error('Error:', err);
res.status(500).end(err.message);
});
});
// });
};
export default prerenderer;
......@@ -9,9 +9,9 @@ import {readFileSync} from 'fs';
import config from './config';
import prerenderer from './middleware/prerenderer';
import genesysProxy from './middleware/genesysProxy';
import robots from './robots';
import i18nServer from '../i18n/i18n-server';
import httpProxy from './middleware/httpProxy';
const i18nextMiddleware = require('i18next-express-middleware'); // has no proper import yet
const app = express();
......@@ -35,7 +35,7 @@ app.use(compression());
// robots.txt
app.get('/robots.txt', robots);
// Proxy all requests starting with /proxy
app.use('/proxy', genesysProxy);
app.use('/proxy', httpProxy);
// Serve static resources (this should be the only thing publicly accessible)
app.use(express.static(path.join('../assets'), {
etag: true,
......
// import {push} from 'react-router-redux';
import { GenesysService } from 'service/GenesysService';
import { AccessionService } from 'service/AccessionService';
import { log } from 'utilities/debug';
export const listAccessions = (filters: object, page: number = 0, results: number = 50) => (dispatch, getState) => {
const token = getState().login.access_token;
return GenesysService.listAccessions(filters, page, results)
.then((data) => {
return data;
}).catch((error) => {
log(`Genesys error`, error);
});
return AccessionService.listAccessions(token, filters, page, results)
.then((data) => {
return data;
}).catch((error) => {
log(`Genesys error`, error);
});
};
export const ACCESSIONS_FILTERFORM = 'Form/Accession/BROWSE';
const clientUrl = `proxy`;
const origin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000';
const clientUrl = `${origin}/proxy`;
export const LOGIN_URL = `${clientUrl}/oauth/token`;
export const LOGOUT_URL = `${clientUrl}/token`;
......
export const GENESYSBROWSE_FILTERFORM = 'Form/Genesys/BROWSE';
// import { ServerInfo } from 'model/serverinfo.model';
// import { SERVER_INFO_URL } from 'constants/apiURLS';
import axios from 'axios';
import authenticatedRequest from 'utilities/requestUtils';
import * as _ from 'lodash';
import {API_BASE_URL} from 'constants/apiURLS';
import {Page} from 'model/common.model';
const API_PREFIX = '/api/genesys';
export class GenesysService {
private static accessToken: any = null;
public static login(): Promise<any> {
if (GenesysService.accessToken === null || GenesysService.accessToken.expireTimestamp < Date.now()) {
return axios.post(`${API_PREFIX}/oauth/token`, null, {
params: {
grant_type: 'client_credentials',
},
})
.then(({ data }) => {
data.expireTimestamp = Date.now() + (data.expires_in - 10) * 1000;
return GenesysService.accessToken = data;
});
} else {
return Promise.resolve(GenesysService.accessToken);
}
}
export class AccessionService {
public static listAccessions(filters: object, page: number = 0, results: number = 50): Promise<Page<object>> {
public static listAccessions(token: string, filters: object, page: number = 0, results: number = 50): Promise<Page<object>> {
const flatFilters = genesysFlatten('', renameFilters(filters), {});
// console.log(`Flatfilters ${JSON.stringify(flatFilters)}`, filters, flatFilters);
return GenesysService.login()
.then((token) => {
// console.log('Genesys token', token);
return authenticatedRequest(token.access_token, {
url: `${API_PREFIX}/v0/acn/filter?p=${page}&l=${results}`,
method: 'POST',
data: {
...flatFilters,
},
});
})
.then(({ data }) => new Page<object>(data));
// TODO catch 403 -- reauth
return authenticatedRequest(token, {
url: `${API_BASE_URL}/acn/filter?p=${page}&l=${results}`,
method: 'POST',
data: {
...flatFilters,
},
}).then(({ data }) => new Page<object>(data));
}
}
......
......@@ -16,7 +16,7 @@ const content = {
const action = {
/*tslint:disable*/
padding: '0 1.143rem',
// padding: '0 1.143rem',
borderTop: 'solid 1px #e0e0e0',
};
......
......@@ -149,7 +149,7 @@ class InternalStringArrField extends React.Component<IStringArrFilterInternal &
endAdornment={
<InputAdornment position="end">
<IconButton type="button" onClick={ this.handleAddCurrent }>
<PlusOne />
<PlusOne style={ { fontSize: '1.5rem' } } />
</IconButton>
</InputAdornment>
}
......
......@@ -29,7 +29,7 @@ class ContentHeader extends React.Component<IContentHeader , any> {
<h1 className="font-bold white mb-5" style={ { marginBottom: 0, fontSize: '1.714rem'} }>
{ title }
</h1>
<Hidden implementation="css" mdDown>
<Hidden implementation="css" smDown>
<h3 className="font-medium white m-0" style={ { marginTop: '.5rem', fontSize: '0.8571rem' } }>
{ subTitle }
</h3>
......
......@@ -68,7 +68,7 @@ const styles = (theme) => ({
marginLeft: '12px',
'& > div > div': {
marginRight: '35px',
[theme.breakpoints.down('md')]: {
[theme.breakpoints.down('sm')]: {
marginRight: '0px',
},
},
......@@ -77,14 +77,14 @@ const styles = (theme) => ({
width: '26px',
height: '26px',
color: '#050708',
[theme.breakpoints.down('md')]: {
[theme.breakpoints.down('sm')]: {
top: '0px',
},
},
'& > div > div:focus': {
backgroundColor: 'transparent',
},
[theme.breakpoints.down('md')]: {
[theme.breakpoints.down('sm')]: {
border: '1px #e8e6e0 solid',
borderRadius: '3px',
padding: '2px 0 0 7px',
......@@ -124,10 +124,13 @@ const styles = (theme) => ({
verticalAlign: {
verticalAlign: 'text-bottom',
paddingLeft: '5px',
[theme.breakpoints.down('md')]: {
[theme.breakpoints.down('sm')]: {
padding: '0 5px 0px 2px',
},
},
inline: {
display: 'inline',
},
}) as StyleRules;
const results = [5, 10, 20, 40, 50, 75, 100];
......@@ -234,12 +237,12 @@ class PaginationComponent extends React.Component<IPaginationComponentProps, any
}
{ ! infinite &&
<div className={ isMobile ? classes.floatRight : classes.floatLeft }>
<Hidden implementation="css" mdUp>
<Hidden implementation="css" mdUp className={ isMobile ? classes.inline : '' }>
<span className={ classes.verticalAlign }>
<span className={ classes.bold }>{ pageObj ? pageObj.number + 1 : 0 }</span>/{ pageObj ? pageObj.totalPages : 0 }
</span>
</Hidden>
<Hidden implementation="css" only={ mobile }>
<Hidden implementation="css" only={ mobile } className={ classes.inline }>
<Button
variant="fab"
color="inherit"
......@@ -259,7 +262,7 @@ class PaginationComponent extends React.Component<IPaginationComponentProps, any
>
<PlayArrow className={ classes.arrowRevert }/>
</Button>
<Hidden implementation="js" only={ mobile }>
<Hidden implementation="js" only={ mobile } className={ classes.inline }>
<span className={ `${classes.checkedPage} ${classes.bold}` }>
{ pageObj ? pageObj.number + 1 : 0 }
</span>
......@@ -273,7 +276,7 @@ class PaginationComponent extends React.Component<IPaginationComponentProps, any
>
<PlayArrow/>
</Button>
<Hidden implementation="css" only={ mobile }>
<Hidden implementation="css" only={ mobile } className={ classes.inline }>
<Button
variant="fab"
color="inherit"
......@@ -284,7 +287,7 @@ class PaginationComponent extends React.Component<IPaginationComponentProps, any
<FastForward/>
</Button>
</Hidden>
<Hidden implementation="css" only={ mobile }>
<Hidden implementation="css" only={ mobile } className={ classes.inline }>
<span className={ classes.verticalAlign }>of <Number value={ pageObj ? pageObj.totalPages : 0 } /> pages</span>
</Hidden>
</div>
......
......@@ -19,13 +19,13 @@ interface ICustomListItemProps extends React.ClassAttributes<any> {
const styles = {
text: {
'& h3': {
'& span': {
fontFamily: 'Roboto-Medium',
color: '#d4d1c6',
},
},
textActive: {
'& h3': {
'& span': {
fontFamily: 'Roboto',
fontWeight: 'bold' as 'bold',
color: '#2b2924',
......
......@@ -22,7 +22,7 @@ const styleSheet = (theme) => ({
background: theme.palette.background.paper,
fontFamily: 'Roboto-Medium',
padding: '0',
'& h3': {
'& span': {
fontFamily: 'Roboto-Medium',
},
boxShadow: '-4px 0px 2px -1px rgba(0, 0, 0, 0.2)',
......
......@@ -16,11 +16,11 @@ const styleSheet = (theme) => ({
height: '100%',
backgroundColor: '#252520',
width: '100%',
[theme.breakpoints.down('sm')]: {
[theme.breakpoints.down('xs')]: {
width: '100vw',
},
'& > li': {
'& > div': {
padding: '0 1.43rem',
borderTop: '1px solid grey',
borderBottom: '2px solid transparent',
......@@ -29,7 +29,7 @@ const styleSheet = (theme) => ({
borderBottom: '3px solid #88ba42',
background: 'inherit',
'&+li': {
'&+div': {
borderTop: 'none',
},
},
......@@ -47,7 +47,7 @@ const styleSheet = (theme) => ({
},
},
'& li:last-child': {
'& div:last-child': {
borderBottom: '1px solid grey',
}
},
......
......@@ -71,11 +71,14 @@ const styleSheet = {
textDecoration: 'none',
},
},
/* tslint:enable */
menuBtn: {
width: '1em',
marginRight: '1.429rem',
'&:hover': {
backgroundColor: 'transparent' as 'transparent',
}
},
/* tslint:enable */
};
interface IHeaderProps extends React.ClassAttributes<any> {
......
......@@ -6,13 +6,13 @@ import { withStyles } from '@material-ui/core/styles';
import {log} from 'utilities/debug';
import { parse } from 'query-string';
import { listAccessions } from 'actions/genesys';
import { listAccessions } from 'actions/accession';
import { Page, Pagination } from 'model/common.model';
import Loading from 'ui/common/Loading';
import PaginationComponent from 'ui/common/pagination';
import GenesysBrowseFilters from './c/Filters';
import AccessionsBrowseFilters from './c/Filters';
import PrettyFilters from 'ui/common/filter/PrettyFilters';
import DOI from 'ui/common/DOI';
......@@ -224,7 +224,7 @@ class BrowsePage extends React.Component<IBrowsePageProps, any> {
return (
<Grid container spacing={ 0 }>
<Grid item xs={ 12 } md={ 3 } lg={ 2 } className={ classes.filterSection }>
<GenesysBrowseFilters initialValues={ filter } onSubmit={ this.applyFilters } />
<AccessionsBrowseFilters initialValues={ filter } onSubmit={ this.applyFilters } />
</Grid>
<Grid item xs={ 12 } md={ 9 } lg={ 10 } className="back-gray">
<Grid container spacing={ 0 }>
......
import * as React from 'react';
import { reduxForm } from 'redux-form';
import { GENESYSBROWSE_FILTERFORM } from 'constants/genesys';
import FiltersBlock from 'ui/common/filter/FiltersBlock';
import CollapsibleComponentSearch from 'ui/common/filter/CollapsibleComponentSearch';
// import StringFilter from 'ui/common/filter/StringFilter';
import StringArrFilter from 'ui/common/filter/StringArrFilter';
// import CropFilter from 'ui/catalog/crop/CropFilter';
// <CollapsibleComponentSearch title="Crop">
// <CropFilter />
// </CollapsibleComponentSearch>
import {ACCESSIONS_FILTERFORM} from 'constants/accession';
const GenesysBrowseFilters = ({handleSubmit, initialize, ...other}) => (
const AccessionsBrowseFilters = ({handleSubmit, initialize, ...other}) => (
<FiltersBlock title="Accessions" handleSubmit={ handleSubmit } initialize={ initialize } { ...other }>
<CollapsibleComponentSearch title="Species">
<StringArrFilter name="crop" label="Crop name" placeholder="barley"/>
......@@ -30,5 +23,5 @@ const GenesysBrowseFilters = ({handleSubmit, initialize, ...other}) => (
export default reduxForm({
enableReinitialize: true,
form: GENESYSBROWSE_FILTERFORM,
})(GenesysBrowseFilters);
form: ACCESSIONS_FILTERFORM,
})(AccessionsBrowseFilters);
......@@ -27,7 +27,7 @@ const styles = (theme) => ({
top: '-50%',
display: 'block' as 'block',
marginBottom: '-100%',
[theme.breakpoints.down('md')]: {
[theme.breakpoints.down('sm')]: {
minWidth: '1000px',
width:'110%',
marginTop:'0px',
......@@ -39,84 +39,88 @@ const styles = (theme) => ({
searchBox: {
textAlign: 'center' as 'center',
padding: '6.857rem 0 5.714rem 0',
[theme.breakpoints.down('md')]: {
[theme.breakpoints.down('sm')]: {
padding: '4.857rem 0 2.714rem 0',
},
},
searchWrapper: {
width: '56%',
margin: '0 auto',
border: '5px solid rgba(0, 0, 0, 0.4)',
borderRight: '9px solid rgba(0, 0, 0, 0.4)',
borderRadius: '10px',
[theme.breakpoints.down('md')]: {
width: '88%',
},
width: '56%',
margin: '0 auto',
border: '5px solid rgba(0, 0, 0, 0.4)',
borderRight: '9px solid rgba(0, 0, 0, 0.4)',
borderRadius: '10px',
[theme.breakpoints.down('sm')]: {
width: '88%',
},
},
searchField: {
width: '100%',
background: 'rgba(255, 255, 255, 0.9)',
border: '2px solid #000',
borderRadius: '4px',
'& input': {
paddingLeft: '1.429rem',
fontSize: '1.286rem',
},
width: '100%',
background: 'rgba(255, 255, 255, 0.9)',
border: '2px solid #000',
borderRadius: '4px',
'& input': {
paddingLeft: '1.429rem',
fontSize: '1.286rem',
},
},
searchButton: {
background: '#0b6eb5',
padding: '0 1.143rem',
outline: '2px solid #0b6eb5',
'& button': {
width: 'auto',
verticalAlign: 'middle',
color: '#fff',
},
'& img': {