Commit ec05fa46 authored by Matija Obreza's avatar Matija Obreza
Browse files

Merge branch '3-entry-page' into 'master'

Resolve web application with expressjs

Closes #3

See merge request grin-global/grin-global-ui!3
parents e20d79da 01171422
......@@ -9,9 +9,10 @@ class WelcomePage extends React.Component<WithTranslation> {
const { t } = this.props;
return (
<>
<h1>{ t('p.welcome.title') }</h1>
<h1>{ t('client:appName') }</h1>
<ul>
<li><Link to="/user">To user list</Link></li>
<li><Link to="/cooperator">To cooperator list</Link></li>
<li><Link to="/login">To login page</Link></li>
</ul>
</>
......
......@@ -43,17 +43,13 @@ export function saveCookies(resp, expireOn: number, apiUrl: string) {
if (resp.refresh_token) {
cookies.set('refresh_token', resp.refresh_token, { domain, path: '/', expires: expDate });
} else {
document.cookie = `refresh_token=; domain=${domain}; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
cookies.remove('refresh_token');
}
}
export function clearCookies() {
const domain = typeof document !== 'undefined' && document.location.origin.includes('.')
? `.${document.location.origin.split('.').filter((item, index, arr) => index > arr.length - 3).join('.')}`
: 'localhost';
log('Clearing cookies');
['authorities', 'access_token', 'refresh_token'].forEach((key) => document.cookie = `${key}=; domain=${domain}; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`);
['authorities', 'access_token', 'refresh_token'].forEach((key) => cookies.remove(key));
}
......@@ -65,15 +61,47 @@ export function isNumeric(value: any): boolean {
* This function remaps reference-by-ID to full objects
*
* Examples:
* dereferenceReferences2(paged.content, { uuid: [ 'owner', 'foobar' ], id: [ 'institute' ]});
* dereferenceReferences2(paged.content, { uuid: [ 'owner', 'foobar' ], id: [ 'institute' ]});
*
* `_self` allows you to handle top-level references:
* dereferenceReferences2(paged.content, { _self: 'user', uuid: [ 'user', 'owner', 'foobar' ], id: [ 'institute' ]});
*
* @param content The list of objects
* @param referenceMap The map of ID-keys of properties
*/
export function dereferenceReferences2(content: any[], referenceMap: object, refs: { [key: string]: any } = {}) {
export function dereferenceReferences(content: any[], referenceMap: any, refs: { [key: string]: any } = {}): any[] {
// Convert cross-references to Partner object
if (!content) {
return;
return content;
}
const { _self, ...refMap } = referenceMap;
let _selfName = null;
if (_self) {
_selfName = Object.keys(refMap).filter((id) => {
// check if mapping for this id contains _self name
return refMap[id].filter((elem) => elem === _self).length === 1;
});
if (_selfName.length === 1) {
_selfName = _selfName[0];
console.log(`_Self ${_self} is by ${_selfName}`);
if (! refs[`${_self}`]) {
refs[`${_self}`] = {}; // initialize map
}
}
} else {
// console.log('No _self found in', referenceMap);
}
if (_selfName) {
// scan for selfs first
content.forEach((entry) => {
if (entry && typeof entry === 'object') {
// add self if declared
// console.log(`Registered _self reference ${_self}.'${entry[_selfName]}' using ${_selfName}`);
refs[`${_self}`][entry[`${_selfName}`]] = entry;
}
});
}
// deep search first
......@@ -81,40 +109,50 @@ export function dereferenceReferences2(content: any[], referenceMap: object, ref
if (entry && typeof entry === 'object') {
for (const p of Object.keys(entry)) {
if (entry[p] && typeof entry[p] === 'object') {
dereferenceReferences2([entry[p]], referenceMap, refs);
dereferenceReferences([entry[p]], refMap, refs);
}
}
}
});
// console.log('Handling references', content);
Object.keys(referenceMap).forEach((id) => {
const props: string[] = referenceMap[id];
Object.keys(refMap).forEach((id) => {
const props: string[] = refMap[id];
// iterate over unique prop names
props.filter((elem, pos, arr) => arr.indexOf(elem) === pos).forEach((props) => {
props.filter((elem, pos, arr) => arr.indexOf(elem) === pos).forEach((prop) => {
const prop0 = Array.isArray(props) ? props[0] : props; // use 1st element as master prop key
if (!refs[prop0]) {
refs[prop0] = {}
} // setup
(Array.isArray(props) ? props : [props]).forEach((prop) => {
// console.log(`Dereferencing ${prop}.${id} using ${prop0}`);
content.filter((entry) => entry && entry[prop] && typeof entry[prop] === 'object' && entry[prop][id])
.map((entry) => entry[prop])
.forEach((ref) => {
refs[prop0][`${ref[id]}`] = ref;
// console.log(`Registered ${prop0}.${id}.'${ref[id]}' in`, refs[prop0]);
});
content.filter((entry) => entry && entry[prop] && typeof entry[prop] !== 'object')
.forEach((entry) => {
// console.log(`Looking up ${prop0}.'${entry[prop]}' in`, refs[prop0]);
entry[prop] = refs[prop0][`${entry[prop]}`] || entry[prop]; // keep original if not found
});
});
// console.log(`Dereferencing ${prop}.${id} as ${prop0}`);
content.filter((entry) => entry && entry[prop] && typeof entry[prop] === 'object' && entry[prop][id])
.map((entry) => entry[prop])
.forEach((ref) => {
refs[prop0][`${ref[id]}`] = ref;
// console.log(`Registered ${prop0}.'${ref[id]}' using ${id} in`, refs[prop0]);
});
content.filter((entry) => entry && entry[prop] && typeof entry[prop] !== 'object')
.forEach((entry) => {
// console.log(`Looking up ${prop0}.'${entry[prop]}' in`, refs[prop0]);
entry[prop] = refs[prop0][`${entry[prop]}`] || entry[prop]; // keep original if not found
});
});
});
if (_selfName) {
content.forEach((entry, index) => {
if (entry && typeof entry !== 'object') {
// add self if declared
// console.log(`Looking up _self reference ${_self}.'${entry}'`);
content[index] = refs[`${_self}`][`${entry}`];
}
});
}
return content;
}
export function insertAtCaret(element, myValue) {
......
export const required = (value, allValues, meta) => !value ? 'client:validations.required' : undefined;
<!DOCTYPE html>
<html lang=SERVER_RENDERED_LANG dir=SERVER_RENDERED_DIR>
<head>
<title>Oh oh</title>
<base href="FRONTEND_PATH" />
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes" />
<meta name="author" content="Genesys Team, helpdesk@genesys-pgr.org" />
<style type="text/css" id="server-side-styles">
body {
font-family: 'Roboto', sans-serif;
padding: 0 !important;
margin: 0;
background: #e7e5df;
overflow-y: scroll !important;
}
</style>
</head>
<body>
<!--[if lt IE 7]>
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
<![endif]-->
<div>
<h1>ERROR_MESSAGE</h1>
<p>ERROR_DETAILS</p>
<p>ERROR_EXPLANATION</p>
</div>
</body>
</html>
<!DOCTYPE html>
<html lang=SERVER_RENDERED_LANG dir=SERVER_RENDERED_DIR>
<head>
<title>SERVER_RENDERED_TITLE</title>
SERVER_RENDERED_HEADLINKS
<base href="FRONTEND_PATH" />
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes" />
<meta name="author" content="Genesys Team, helpdesk@genesys-pgr.org" />
<style type="text/css" id="server-side-styles">
SERVER_RENDERED_CSS
</style>
SERVER_RENDERED_BUNDLECSS
</head>
<body>
<!--[if lt IE 7]>
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
<![endif]-->
<div id="the-app">SERVER_RENDERED_HTML</div>
<script>
__PRELOADED_STATE__ = SERVER_RENDERED_STATE;
window.initialLanguage = SERVER_RENDERED_LANG;
window.initialI18nStore = SERVER_RENDERED_I18NSTORE;
window.localesMapping = <%= JSON.stringify(htmlWebpackPlugin.options.localesMapping) %>;
window.softwareVersion = <%= JSON.stringify(htmlWebpackPlugin.options.version) %>;
window.softwareCommit = <%= JSON.stringify(htmlWebpackPlugin.options.commithash) %>;
</script>
SERVER_RENDERED_BUNDLESCRIPTS
</body>
</html>
const path = require('path');
const webpack = require('webpack');
const MinifyPlugin = require('terser-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
// other
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
const ALLOW_ROBOTS = process.env.ALLOW_ROBOTS === 'true';
const ifThenElse = (condition, ifTrue, ifFalse) => condition ? ifTrue : ifFalse;
module.exports = {
name: 'server',
target: 'node',
mode: ifThenElse(IS_PRODUCTION, 'production', 'development'),
entry: {
server: ['./server/index.ts']
},
output: {
filename: '[name].js',
chunkFilename: '[name].js',
publicPath: '',
path: path.join(process.cwd(), 'target/app/server')
},
resolve: {
extensions: [
'.ts', '.tsx', '.js', '.jsx'
],
modules: [path.resolve('./src'), path.resolve('./server'), 'node_modules'],
alias: {
'@gringlobal/client': path.resolve(__dirname, '../../client/src'),
'@gringlobal/i18n': path.resolve(__dirname, '../../i18n/src'),
'server': path.resolve(__dirname, '../server'),
}
},
module: {
rules: [
{
test: /\.tsx?$/,
use: [
{
loader: 'awesome-typescript-loader',
},
],
},
]
},
plugins: [
// https://facebook.github.io/react/docs/optimizing-performance.html#use-the-production-build
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(IS_PRODUCTION ? 'production' : 'development'),
ALLOW_ROBOTS: JSON.stringify(ALLOW_ROBOTS ? 'true' : 'false'),
}
}),
new CopyWebpackPlugin([
{ from: 'react-loadable.json', to: 'react-loadable.json' },
]),
],
optimization: {
namedModules: true,
noEmitOnErrors: true,
// NOTE: Don't use splitChunks for server code
// splitChunks: {
// chunks: 'all',
// cacheGroups: {
// vendor: {
// test: /[\\/]node_modules[\\/]/,
// name: 'vendors',
// chunks: 'all'
// }
// },
// },
minimizer: ifThenElse(IS_PRODUCTION, [
//
new MinifyPlugin({
parallel: true,
terserOptions: {
ecma: undefined,
warnings: false,
parse: {},
compress: {},
mangle: false, // Note `mangle.properties` is `false` by default.
module: false,
output: null,
toplevel: false,
nameCache: null,
ie8: false,
keep_classnames: true,
keep_fnames: true,
safari10: false,
},
}),
],
// is not production
[]
),
}
};
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin');
const HtmlWebpackExcludeAssetsPlugin = require('html-webpack-exclude-assets-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const WebpackChunkHash = require('webpack-chunk-hash');
const ManifestPlugin = require('webpack-manifest-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const GitRevisionPlugin = require('git-revision-webpack-plugin');
const gitRevisionPlugin = new GitRevisionPlugin();
const ReactLoadable = require('react-loadable/webpack');
const fs = require('fs');
// devserver configuration
const HOST = process.env.HOST || 'localhost';
const PORT = process.env.PORT || 3000;
// other...
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
const SSR = process.env.SSR;
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) {
const index1 = list.indexOf(chunk1.names[0]);
const index2 = list.indexOf(chunk2.names[0]);
if (index2 === -1 || index1 < index2) {
return -1;
}
if (index1 === -1 || index1 > index2) {
return 1;
}
return 0;
};
};
module.exports = {
stats: {
colors: true
},
devServer: {
hot: true,
inline: true,
contentBase: path.join(__dirname, '../'),
compress: true,
port: PORT,
host: HOST,
overlay: {
warnings: true,
errors: true
},
clientLogLevel: 'warning',
historyApiFallback: {
disableDotRule: true
},
proxy: {
'/proxy': {
target: API_URL,
logLevel: 'debug',
ws: true,
// secure: false,
pathRewrite(path, req) {
let p = path.replace('/proxy', '');
if (p.startsWith('/oauth/token')) {
const grantType = req.query.grant_type;
if (grantType === 'refresh_token') {
const refreshToken = req.query.refresh_token;
if (!refreshToken) {
// eslint-disable-next-line camelcase
req.query.grant_type = 'client_credentials';
p = p.replace('refresh_token', 'client_credentials');
}
}
if(grantType === 'client_credentials' || grantType === 'password' || grantType === 'refresh_token') {
p = `${p}&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}`;
}
// We need some, if not all headers
// // remove all headers from request
// req.headers = {};
} else if (p.indexOf('/google/verify-token') === 0) {
p = `${p}&clientId=${CLIENT_ID}`;
}
// 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('Outgoing headers ' + p, req.headers);
return p;
},
onError(err, req, res) {
console.log(err);
},
onClose(res, socket, head) {
// view disconnected websocket connections
console.log('Client disconnected', res);
}
}
},
},
entry: {
gringlobal: [ 'babel-polyfill', './entrypoints/vendor.ts', './entrypoints/client.tsx' ],
},
output: {
filename: '[name].[hash].js',
chunkFilename: '[name].[hash].js',
path: path.join(process.cwd(), 'target/app/assets'),
publicPath: ''
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx'],
modules: [path.resolve('./src'), path.resolve('./server'), 'node_modules'],
alias: {
'@gringlobal/client': path.resolve(__dirname, '../../client/src'),
'@gringlobal/i18n': path.resolve(__dirname, '../../i18n/src'),
'server': path.resolve(__dirname, '../server'),
}
},
module: {
rules: [
{
test: /\.tsx?$/,
enforce: 'pre',
loader: 'eslint-loader',
exclude: /node_modules/,
},
{
test: /\.tsx?$/,
use: [
{
loader: 'awesome-typescript-loader',
},
],
},
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
modules: false
},
},
'resolve-url-loader', {
loader: 'sass-loader',
options: {
sourceMap: true,
sassOptions: {
includePaths: ['node_modules']
}
}
}
]
},
{
test: /\.css$/,
exclude: /node_modules\/react\-toolbox/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
},
{
test: /\.(woff|woff2|eot)(\?.*)?$/,
loader: 'file-loader?name=fonts/[hash].[ext]'
},
{
test: /\.ttf(\?.*)?$/,
loader: 'url-loader?limit=10000&mimetype=application/octet-stream&name=fonts/[hash].[ext]'
},
{
test: /\.(jpe?g|png|gif|svg)$/i,
exclude: /node_modules\/leaflet/,
loader: 'url-loader?limit=100000&name=images/[name].[ext]'
},
{
test: /\.(jpe?g|png|gif|svg)$/i,
include: /node_modules\/leaflet/,
loader: 'file-loader?limit=100000&name=images/[name].[ext]'
}
]
},
plugins: [
/* new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: Infinity
}),*/
// Git revision
new GitRevisionPlugin(),
// MD5 chunkhash
new WebpackChunkHash(),
// We use the generated manifest.js to look for current server.[hash].js and vendor.[chunkhash].js
new ManifestPlugin({
basePath: '/html/'
}),
// Inject scripts to app's real index.html
new HtmlWebpackPlugin({
title: 'GRIN-Global Client', // The title to use for the generated HTML document
filename: 'index.html', // The file to write the HTML to
// minify: {...}, // Pass a html-minifier options object to minify the output
xhtml: true,
excludeAssets: [/server(\..+)*\.js/],
template: './entrypoints/index.html',
// chunksSortMode: sortedChunks(['vendor', 'common', 'gringlobal']),
// chunks: ['vendor', 'common', 'gringlobal'],
favicon: 'favicon.ico',
version: gitRevisionPlugin.version(),
commithash: gitRevisionPlugin.commithash(),
localesMapping: fs.readFileSync('./generated/locales/localesMapping.json', { encoding: 'utf8' }),
}),
// Inject scripts for SSR
new HtmlWebpackPlugin({
title: 'GRIN-Global Client', // The title to use for the generated HTML document
filename: 'ssr-compiled.html', // The file to write the HTML to
// minify: {...}, // Pass a html-minifier options object to minify the output
xhtml: true,
excludeAssets: [/server(\..+)*\.js/],
template: './assets/html/ssr-template.html',
// chunksSortMode: sortedChunks(['vendor', 'common', 'gringlobal']),
// chunks: ['vendor', 'common', 'gringlobal'],
favicon: 'favicon.ico',
version: gitRevisionPlugin.version(),
commithash: gitRevisionPlugin.commithash(),