@proteinjs/server 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/LICENSE +21 -0
- package/dist/generated/index.d.ts +19 -0
- package/dist/generated/index.d.ts.map +1 -0
- package/dist/generated/index.js +47 -0
- package/dist/generated/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/jest.config.d.ts +8 -0
- package/dist/jest.config.d.ts.map +1 -0
- package/dist/jest.config.js +20 -0
- package/dist/jest.config.js.map +1 -0
- package/dist/src/NodeSessionDataStorage.d.ts +10 -0
- package/dist/src/NodeSessionDataStorage.d.ts.map +1 -0
- package/dist/src/NodeSessionDataStorage.js +42 -0
- package/dist/src/NodeSessionDataStorage.js.map +1 -0
- package/dist/src/loadRoutes.d.ts +5 -0
- package/dist/src/loadRoutes.d.ts.map +1 -0
- package/dist/src/loadRoutes.js +212 -0
- package/dist/src/loadRoutes.js.map +1 -0
- package/dist/src/routes/healthCheck.d.ts +3 -0
- package/dist/src/routes/healthCheck.d.ts.map +1 -0
- package/dist/src/routes/healthCheck.js +51 -0
- package/dist/src/routes/healthCheck.js.map +1 -0
- package/dist/src/routes/reactApp.d.ts +7 -0
- package/dist/src/routes/reactApp.d.ts.map +1 -0
- package/dist/src/routes/reactApp.js +111 -0
- package/dist/src/routes/reactApp.js.map +1 -0
- package/dist/src/startServer.d.ts +3 -0
- package/dist/src/startServer.d.ts.map +1 -0
- package/dist/src/startServer.js +238 -0
- package/dist/src/startServer.js.map +1 -0
- package/dist/webpack.config.d.ts +74 -0
- package/dist/webpack.config.d.ts.map +1 -0
- package/dist/webpack.config.js +111 -0
- package/dist/webpack.config.js.map +1 -0
- package/generated/index.ts +43 -0
- package/index.ts +1 -0
- package/jest.config.js +18 -0
- package/package.json +77 -0
- package/src/NodeSessionDataStorage.ts +40 -0
- package/src/loadRoutes.ts +126 -0
- package/src/routes/healthCheck.ts +10 -0
- package/src/routes/reactApp.ts +57 -0
- package/src/startServer.ts +184 -0
- package/tsconfig.json +18 -0
- package/webpack.config.js +112 -0
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@proteinjs/server",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "A server impl",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/brentbahry/server.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/brentbahry/server/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/brentbahry/server#readme",
|
|
16
|
+
"author": "Brent Bahry",
|
|
17
|
+
"license": "ISC",
|
|
18
|
+
"scripts": {
|
|
19
|
+
"clean": "rm -rf dist/ generated/ node_modules/ package-lock.json",
|
|
20
|
+
"build": "reflection-build && tsc",
|
|
21
|
+
"watch": "reflection-watch",
|
|
22
|
+
"dev": "DEVELOPMENT=true node dist/generated/index.js"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@proteinjs/db": "1.0.3",
|
|
26
|
+
"@proteinjs/reflection": "1.0.2",
|
|
27
|
+
"@proteinjs/server-api": "1.0.2",
|
|
28
|
+
"@proteinjs/util": "1.0.18",
|
|
29
|
+
"body-parser": "1.19.0",
|
|
30
|
+
"compression": "1.7.4",
|
|
31
|
+
"cookie-parser": "1.4.5",
|
|
32
|
+
"express": "4.17.1",
|
|
33
|
+
"express-session": "1.17.1",
|
|
34
|
+
"passport": "0.4.1",
|
|
35
|
+
"passport-local": "1.0.0",
|
|
36
|
+
"react": "18.2.0",
|
|
37
|
+
"react-helmet": "6.1.0",
|
|
38
|
+
"webpack": "5.88.2",
|
|
39
|
+
"webpack-dev-middleware": "7.0.0",
|
|
40
|
+
"webpack-hot-middleware": "2.26.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@emotion/react": "11.11.1",
|
|
44
|
+
"@emotion/styled": "11.11.0",
|
|
45
|
+
"@mui/icons-material": "5.14.11",
|
|
46
|
+
"@mui/joy": "5.0.0-beta.8",
|
|
47
|
+
"@mui/material": "5.14.11",
|
|
48
|
+
"@pmmmwh/react-refresh-webpack-plugin": "0.5.11",
|
|
49
|
+
"@proteinjs/reflection-build": "1.0.5",
|
|
50
|
+
"@types/body-parser": "1.19.0",
|
|
51
|
+
"@types/compression": "1.7.0",
|
|
52
|
+
"@types/cookie-parser": "1.4.2",
|
|
53
|
+
"@types/express": "4.17.7",
|
|
54
|
+
"@types/express-session": "1.17.0",
|
|
55
|
+
"@types/node": "14.0.13",
|
|
56
|
+
"@types/passport": "1.0.4",
|
|
57
|
+
"@types/passport-local": "1.0.33",
|
|
58
|
+
"@types/react": "18.2.23",
|
|
59
|
+
"@types/react-helmet": "6.1.0",
|
|
60
|
+
"@types/webpack-hot-middleware": "2.25.9",
|
|
61
|
+
"buffer": "6.0.3",
|
|
62
|
+
"crypto-browserify": "3.12.0",
|
|
63
|
+
"events": "3.3.0",
|
|
64
|
+
"file-loader": "6.2.0",
|
|
65
|
+
"process": "0.11.10",
|
|
66
|
+
"react-refresh": "0.14.0",
|
|
67
|
+
"react-refresh-typescript": "2.0.9",
|
|
68
|
+
"stream-browserify": "3.0.0",
|
|
69
|
+
"ts-loader": "8.0.3",
|
|
70
|
+
"typescript": "5.2.2",
|
|
71
|
+
"url": "0.11.3",
|
|
72
|
+
"util": "0.12.5"
|
|
73
|
+
},
|
|
74
|
+
"main": "./dist/generated/index.js",
|
|
75
|
+
"types": "./dist/generated/index.d.ts",
|
|
76
|
+
"gitHead": "7609528bc63fb92c7b528fc31795bef1e6913cbf"
|
|
77
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import asyncHooks from 'async_hooks';
|
|
2
|
+
import { SessionData, SessionDataStorage } from '@proteinjs/server-api';
|
|
3
|
+
|
|
4
|
+
export class NodeSessionDataStorage implements SessionDataStorage {
|
|
5
|
+
private static HOOK_INITIALIZED = false;
|
|
6
|
+
private static readonly SESSION_DATA: {[id: string]: SessionData} = {};
|
|
7
|
+
environment = 'node' as 'node';
|
|
8
|
+
|
|
9
|
+
setData(data: SessionData) {
|
|
10
|
+
if (!NodeSessionDataStorage.HOOK_INITIALIZED)
|
|
11
|
+
this.initHook();
|
|
12
|
+
|
|
13
|
+
if (NodeSessionDataStorage.SESSION_DATA[asyncHooks.executionAsyncId()])
|
|
14
|
+
return;
|
|
15
|
+
|
|
16
|
+
NodeSessionDataStorage.SESSION_DATA[asyncHooks.executionAsyncId()] = data;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getData(): SessionData {
|
|
20
|
+
if (!NodeSessionDataStorage.HOOK_INITIALIZED)
|
|
21
|
+
this.initHook();
|
|
22
|
+
|
|
23
|
+
return NodeSessionDataStorage.SESSION_DATA[asyncHooks.executionAsyncId()];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private initHook() {
|
|
27
|
+
asyncHooks.createHook({
|
|
28
|
+
init: (asyncId: number, type: string, triggerAsyncId: number, resource: Object) => {
|
|
29
|
+
if (!NodeSessionDataStorage.SESSION_DATA[triggerAsyncId])
|
|
30
|
+
return;
|
|
31
|
+
|
|
32
|
+
NodeSessionDataStorage.SESSION_DATA[asyncId] = NodeSessionDataStorage.SESSION_DATA[triggerAsyncId];
|
|
33
|
+
},
|
|
34
|
+
destroy: (asyncId: number) => {
|
|
35
|
+
delete NodeSessionDataStorage.SESSION_DATA[asyncId];
|
|
36
|
+
}
|
|
37
|
+
}).enable();
|
|
38
|
+
NodeSessionDataStorage.HOOK_INITIALIZED = true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import passport from 'passport';
|
|
3
|
+
import { ServerConfig, Route, getRequestListeners, Session, SessionData, getSessionDataCaches } from '@proteinjs/server-api';
|
|
4
|
+
import { createReactApp } from './routes/reactApp';
|
|
5
|
+
import { Logger } from '@proteinjs/util';
|
|
6
|
+
|
|
7
|
+
const logger = new Logger('Server');
|
|
8
|
+
|
|
9
|
+
export function loadRoutes(routes: Route[], server: express.Express, config: ServerConfig) {
|
|
10
|
+
let starRoute: Route|null = null;
|
|
11
|
+
const wildcardRoutes: Route[] = [];
|
|
12
|
+
for (const route of routes) {
|
|
13
|
+
logger.info(`Loading route: ${route.path}`);
|
|
14
|
+
if (route.path == '*') {
|
|
15
|
+
starRoute = route;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (route.path.includes('*')) {
|
|
20
|
+
wildcardRoutes.push(route);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
server[route.method](getPath(route.path), wrapRoute(route.onRequest.bind(route), config));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const wildcardRoute of wildcardRoutes)
|
|
28
|
+
server[wildcardRoute.method](getPath(wildcardRoute.path), wrapRoute(wildcardRoute.onRequest.bind(wildcardRoute), config));
|
|
29
|
+
|
|
30
|
+
if (starRoute)
|
|
31
|
+
server[starRoute.method](starRoute.path, wrapRoute(starRoute.onRequest.bind(starRoute), config));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function loadDefaultStarRoute(routes: Route[], server: express.Express, config: ServerConfig) {
|
|
35
|
+
let starRouteSpecified = false;
|
|
36
|
+
for (const route of routes) {
|
|
37
|
+
if (route.path == '*') {
|
|
38
|
+
starRouteSpecified = true;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!starRouteSpecified && config.staticContent?.bundlePaths) {
|
|
44
|
+
const reactApp = createReactApp(config);
|
|
45
|
+
server[reactApp.method](reactApp.path, wrapRoute(reactApp.onRequest, config));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getPath(path: string) {
|
|
50
|
+
return path.startsWith('/') ? path : `/${path}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function wrapRoute(route: (request: express.Request, response: express.Response) => Promise<void>, config: ServerConfig) {
|
|
54
|
+
return async function (request: express.Request, response: express.Response, next: express.NextFunction) {
|
|
55
|
+
if (response.locals['responseHandled']) {
|
|
56
|
+
next();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (config.authenticate) {
|
|
61
|
+
await new Promise<void>((resolve, reject) => {
|
|
62
|
+
passport.authenticate('local', function (err, user, info) {
|
|
63
|
+
if (err)
|
|
64
|
+
reject(err);
|
|
65
|
+
|
|
66
|
+
resolve();
|
|
67
|
+
})(request, response, next);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const sessionData: SessionData = { sessionId: request.sessionID, user: request.user as string, data: {} };
|
|
72
|
+
for (const sessionDataCache of getSessionDataCaches())
|
|
73
|
+
sessionData.data[sessionDataCache.key] = await sessionDataCache.create(sessionData.sessionId, sessionData.user);
|
|
74
|
+
Session.setData(sessionData);
|
|
75
|
+
|
|
76
|
+
const requestListeners = getRequestListeners();
|
|
77
|
+
for (const listener of requestListeners) {
|
|
78
|
+
if (!listener.beforeRequest)
|
|
79
|
+
continue;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
await listener.beforeRequest(request, response);
|
|
83
|
+
} catch (error: any) {
|
|
84
|
+
logger.error(`Caught error when running listener before request`, error);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const sixtyMinutes = 1000 * 60 * 60;
|
|
89
|
+
const timeout = typeof config.request?.timeoutMs !== 'undefined' ? config.request.timeoutMs : sixtyMinutes;
|
|
90
|
+
request.setTimeout(timeout, () => {
|
|
91
|
+
if (response.locals.requestNumber)
|
|
92
|
+
logger.warn(`[#${response.locals.requestNumber}] Timed out ${request.originalUrl}`);
|
|
93
|
+
else
|
|
94
|
+
logger.warn(`Timed out ${request.originalUrl}`);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await route(request, response);
|
|
99
|
+
} catch(error) {
|
|
100
|
+
console.error(error);
|
|
101
|
+
}
|
|
102
|
+
response.locals['responseHandled'] = true;
|
|
103
|
+
|
|
104
|
+
for (const listener of requestListeners) {
|
|
105
|
+
if (!listener.afterRequest)
|
|
106
|
+
continue;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
await listener.afterRequest(request, response);
|
|
110
|
+
} catch (error: any) {
|
|
111
|
+
logger.error(`Caught error when running listener after request`, error);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
next();
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function basicAuthCredentials(request: express.Request): { username: string, password: string } | null {
|
|
120
|
+
const b64auth = (request.headers.authorization || '').split(' ')[1] || '';
|
|
121
|
+
const [username, password] = Buffer.from(b64auth, 'base64').toString().split(':');
|
|
122
|
+
if (!username || !password)
|
|
123
|
+
return null;
|
|
124
|
+
|
|
125
|
+
return { username, password };
|
|
126
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import ReactHelmet from 'react-helmet';
|
|
3
|
+
import { ServerConfig, getServerRenderedScripts } from '@proteinjs/server-api';
|
|
4
|
+
|
|
5
|
+
export const createReactApp = (serverConfig: ServerConfig) => {
|
|
6
|
+
return {
|
|
7
|
+
path: '*',
|
|
8
|
+
method: 'get' as 'get',
|
|
9
|
+
onRequest: async (request: any, response: any): Promise<void> => {
|
|
10
|
+
if (request.path.startsWith('/static'))
|
|
11
|
+
return;
|
|
12
|
+
|
|
13
|
+
if (!serverConfig.staticContent?.bundlePaths)
|
|
14
|
+
throw new Error(`ServerConfig.bundlePath must be provided to serve react app`);
|
|
15
|
+
|
|
16
|
+
const helmet = ReactHelmet.renderStatic();
|
|
17
|
+
response.send(`<!DOCTYPE html>
|
|
18
|
+
<html ${helmet.htmlAttributes}>
|
|
19
|
+
<head>
|
|
20
|
+
<meta charset='utf-8' />
|
|
21
|
+
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
|
22
|
+
<link href='${serverConfig.staticContent?.faviconPath ? path.join('/static/', serverConfig.staticContent.faviconPath) : ''}' rel='icon' type='image/x-icon' />
|
|
23
|
+
${helmet.title.toString()}
|
|
24
|
+
${helmet.meta.toString()}
|
|
25
|
+
${helmet.link.toString()}
|
|
26
|
+
</head>
|
|
27
|
+
<body ${helmet.bodyAttributes.toString()}>
|
|
28
|
+
<div id='app'></div>
|
|
29
|
+
<script>proteinjs = {};</script>
|
|
30
|
+
${await serverRenderedScriptTags()}
|
|
31
|
+
${bundleScriptTags(serverConfig)}
|
|
32
|
+
</body>
|
|
33
|
+
</html>`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function bundleScriptTags(serverConfig: ServerConfig) {
|
|
40
|
+
if (!serverConfig.staticContent?.bundlePaths)
|
|
41
|
+
return;
|
|
42
|
+
|
|
43
|
+
const scriptTags: string[] = [];
|
|
44
|
+
for (const bundlePath of serverConfig.staticContent.bundlePaths)
|
|
45
|
+
scriptTags.push(`<script src='${path.join('/static/', bundlePath)}'></script>`);
|
|
46
|
+
|
|
47
|
+
return scriptTags.join('\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function serverRenderedScriptTags() {
|
|
51
|
+
const scripts = getServerRenderedScripts();
|
|
52
|
+
const scriptTags: string[] = [];
|
|
53
|
+
for (const script of scripts)
|
|
54
|
+
scriptTags.push(`<script>${await script.script()}</script>`);
|
|
55
|
+
|
|
56
|
+
return scriptTags.join('\n');
|
|
57
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import expressSession from 'express-session';
|
|
3
|
+
import compression from 'compression';
|
|
4
|
+
import cookieParser from 'cookie-parser';
|
|
5
|
+
import bodyParser from 'body-parser';
|
|
6
|
+
import passport from 'passport';
|
|
7
|
+
import passportLocal from 'passport-local';
|
|
8
|
+
import webpack from 'webpack';
|
|
9
|
+
import webpackDevMiddleware from 'webpack-dev-middleware';
|
|
10
|
+
import webpackHotMiddleware from 'webpack-hot-middleware';
|
|
11
|
+
import { Db } from '@proteinjs/db';
|
|
12
|
+
import { ServerConfig, getRoutes } from '@proteinjs/server-api';
|
|
13
|
+
import { loadRoutes, loadDefaultStarRoute } from './loadRoutes';
|
|
14
|
+
import { Logger } from '@proteinjs/util';
|
|
15
|
+
|
|
16
|
+
const webpackConfig = require('../webpack.config');
|
|
17
|
+
const staticContentPath = '/static/';
|
|
18
|
+
const logger = new Logger('Server');
|
|
19
|
+
|
|
20
|
+
export async function startServer(config: ServerConfig) {
|
|
21
|
+
const routes = getRoutes();
|
|
22
|
+
await runStartupEvents(config);
|
|
23
|
+
const server = express();
|
|
24
|
+
configureRequests(server);
|
|
25
|
+
initializeHotReloading(server, config);
|
|
26
|
+
beforeRequest(server, config);
|
|
27
|
+
loadRoutes(routes.filter(route => route.useHttp), server, config);
|
|
28
|
+
configureHttps(server); // registering here forces static content to be redirected to https
|
|
29
|
+
configureStaticContentRouter(server, config); // registering here prevents sessions from being created on static content requests
|
|
30
|
+
configureSession(server, config);
|
|
31
|
+
loadRoutes(routes.filter(route => !route.useHttp), server, config);
|
|
32
|
+
loadDefaultStarRoute(routes, server, config);
|
|
33
|
+
afterRequest(server, config);
|
|
34
|
+
start(server, config);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function runStartupEvents(config: ServerConfig) {
|
|
38
|
+
await new Db().init();
|
|
39
|
+
|
|
40
|
+
if (config.onStartup)
|
|
41
|
+
await config.onStartup();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function configureRequests(server: express.Express) {
|
|
45
|
+
server.use(compression());
|
|
46
|
+
server.use(cookieParser());
|
|
47
|
+
server.use(bodyParser.json({ limit: '100mb' }));
|
|
48
|
+
server.use(bodyParser.urlencoded({
|
|
49
|
+
extended: true,
|
|
50
|
+
limit: '100mb'
|
|
51
|
+
}));
|
|
52
|
+
server.disable('x-powered-by');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function initializeHotReloading(server: express.Express, config: ServerConfig) {
|
|
56
|
+
if (!process.env.DEVELOPMENT || process.env.DISABLE_HOT_CLIENT_BUILDS || config.disableHotClientBuilds || !config.staticContent?.staticContentDir || !config.staticContent?.appEntryPath)
|
|
57
|
+
return;
|
|
58
|
+
|
|
59
|
+
let wpConfig = Object.assign({}, webpackConfig);
|
|
60
|
+
wpConfig['entry'] = { app: ['webpack-hot-middleware/client', config.staticContent.appEntryPath] };
|
|
61
|
+
wpConfig['output']['path'] = config.staticContent.staticContentDir;
|
|
62
|
+
wpConfig['output']['publicPath'] = staticContentPath;
|
|
63
|
+
const webpackCompiler = webpack(wpConfig);
|
|
64
|
+
server.use(webpackDevMiddleware(webpackCompiler, {
|
|
65
|
+
publicPath: staticContentPath,
|
|
66
|
+
}));
|
|
67
|
+
server.use(webpackHotMiddleware(webpackCompiler));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function configureHttps(server: express.Express) {
|
|
71
|
+
server.use((request: express.Request, response: express.Response, next: express.NextFunction) => {
|
|
72
|
+
if (request.protocol == 'https' || response.headersSent || process.env.DEVELOPMENT) {
|
|
73
|
+
next();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
logger.debug(`Redirecting to https: ${request.headers.host + request.url}`);
|
|
78
|
+
response.redirect('https://' + request.headers.host + request.url);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function configureStaticContentRouter(server: express.Express, config: ServerConfig) {
|
|
83
|
+
if (!config.staticContent?.staticContentDir)
|
|
84
|
+
return;
|
|
85
|
+
|
|
86
|
+
server.use(staticContentPath, express.static(config.staticContent.staticContentDir));
|
|
87
|
+
logger.info(`Serving static content on path: ${staticContentPath}, serving from directory: ${config.staticContent.staticContentDir}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function configureSession(server: express.Express, config: ServerConfig) {
|
|
91
|
+
const sixtyDays = 1000 * 60 * 60 * 24 * 60;
|
|
92
|
+
let sessionOptions: expressSession.SessionOptions = {
|
|
93
|
+
secret: config.session.secret,
|
|
94
|
+
store: config.session.store,
|
|
95
|
+
resave: false,
|
|
96
|
+
saveUninitialized: false,
|
|
97
|
+
cookie: {
|
|
98
|
+
maxAge: sixtyDays
|
|
99
|
+
},
|
|
100
|
+
rolling: true
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
if (!process.env.DEVELOPMENT) {
|
|
104
|
+
server.set('trust proxy', 1);
|
|
105
|
+
if (!sessionOptions.cookie)
|
|
106
|
+
sessionOptions.cookie = {};
|
|
107
|
+
sessionOptions.cookie.secure = true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (config.session)
|
|
111
|
+
sessionOptions = Object.assign(sessionOptions, config.session);
|
|
112
|
+
|
|
113
|
+
server.use(expressSession(sessionOptions));
|
|
114
|
+
server.use(passport.initialize());
|
|
115
|
+
server.use(passport.session());
|
|
116
|
+
if (config.authenticate)
|
|
117
|
+
initializeAuthentication(config.authenticate);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function initializeAuthentication(authenticate: (username: string, password: string) => Promise<true | string>) {
|
|
121
|
+
passport.use(new passportLocal.Strategy(async function (username, password, done) {
|
|
122
|
+
logger.info(`Authenticating`);
|
|
123
|
+
const result = await authenticate(username, password);
|
|
124
|
+
if (result === true)
|
|
125
|
+
return done(null, { username });
|
|
126
|
+
|
|
127
|
+
return done(new Error(result));
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
passport.serializeUser(function (user, done) {
|
|
131
|
+
done(null, user);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
passport.deserializeUser(function (id, done) {
|
|
135
|
+
done(null, id);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function beforeRequest(server: express.Express, config: ServerConfig) {
|
|
140
|
+
let requestCounter: number = 0;
|
|
141
|
+
if (config.request?.disableRequestLogging == false || typeof config.request?.disableRequestLogging === 'undefined') {
|
|
142
|
+
server.use((request: express.Request, response: express.Response, next: express.NextFunction) => {
|
|
143
|
+
if (request.path.startsWith('/static') || request.path.startsWith('/favicon.ico')) {
|
|
144
|
+
next();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const requestNumber = ++requestCounter;
|
|
149
|
+
logger.info(`[#${requestNumber}] Started ${request.originalUrl}`);
|
|
150
|
+
response.locals = { requestNumber };
|
|
151
|
+
next();
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (config.request?.beforeRequest)
|
|
156
|
+
server.use(config.request.beforeRequest);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function afterRequest(server: express.Express, config: ServerConfig) {
|
|
160
|
+
if (config.request?.afterRequest)
|
|
161
|
+
server.use(config.request.afterRequest);
|
|
162
|
+
|
|
163
|
+
if (config.request?.disableRequestLogging == false || typeof config.request?.disableRequestLogging === 'undefined') {
|
|
164
|
+
server.use((request: express.Request, response: express.Response, next: express.NextFunction) => {
|
|
165
|
+
if (request.path.startsWith('/static') || request.path.startsWith('/favicon.ico')) {
|
|
166
|
+
next();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
logger.info(`[#${response.locals.requestNumber}] Finished ${request.originalUrl}`);
|
|
171
|
+
next();
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function start(server: express.Express, config: ServerConfig) {
|
|
177
|
+
const port = config.port ? config.port : 3000;
|
|
178
|
+
server.listen(port, () => {
|
|
179
|
+
if (process.env.DEVELOPMENT)
|
|
180
|
+
logger.info(`Starting in development mode`);
|
|
181
|
+
|
|
182
|
+
logger.info(`Server listening on port: ${port}`);
|
|
183
|
+
});
|
|
184
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"rootDir": "./",
|
|
4
|
+
"target": "es5",
|
|
5
|
+
"module": "commonjs",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"sourceMap": true,
|
|
9
|
+
"outDir": "./dist/",
|
|
10
|
+
"strict": true,
|
|
11
|
+
"noImplicitAny": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"allowJs": true
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
var path = require('path');
|
|
2
|
+
var webpack = require('webpack');
|
|
3
|
+
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
|
|
4
|
+
const ReactRefreshTypeScript = require('react-refresh-typescript');
|
|
5
|
+
|
|
6
|
+
// if there are module resolution issues, verify assumptions about pathing
|
|
7
|
+
// console.log(`__dirname in webpack.config: ${__dirname}`);
|
|
8
|
+
|
|
9
|
+
// see https://github.com/webpack/webpack/issues/11467#issuecomment-808618999/
|
|
10
|
+
// for details
|
|
11
|
+
const webpack5esmInteropRule = {
|
|
12
|
+
test: /\.m?js/,
|
|
13
|
+
resolve: {
|
|
14
|
+
fullySpecified: false
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
mode: 'development',
|
|
20
|
+
devtool: 'inline-source-map',
|
|
21
|
+
// entry provided dynamically in startServer.initializeHotReloading
|
|
22
|
+
// entry: {
|
|
23
|
+
// app: ['webpack-hot-middleware/client', './generated/index.ts'],
|
|
24
|
+
// },
|
|
25
|
+
output: {
|
|
26
|
+
filename: '[name].js',
|
|
27
|
+
// path and publicPath provided in startServer.initializeHotReloading
|
|
28
|
+
// path: path.join(__dirname, 'dist'),
|
|
29
|
+
library: '[name]'
|
|
30
|
+
},
|
|
31
|
+
resolve: {
|
|
32
|
+
extensions: ['.tsx', '.ts', '.js', '.json'],
|
|
33
|
+
alias: {
|
|
34
|
+
'react': path.join(__dirname, '../node_modules/react'),
|
|
35
|
+
'process': 'process/browser',
|
|
36
|
+
'@mui/joy': path.join(__dirname, '../node_modules/@mui/joy'),
|
|
37
|
+
'@mui/material': path.join(__dirname, '../node_modules/@mui/material'),
|
|
38
|
+
'@mui/icons-material': path.join(__dirname, '../node_modules/@mui/icons-material'),
|
|
39
|
+
'webpack-hot-middleware': path.join(__dirname, '../node_modules/webpack-hot-middleware'),
|
|
40
|
+
},
|
|
41
|
+
// provide shims for node libraries for webpack >= 5
|
|
42
|
+
fallback: {
|
|
43
|
+
'crypto': require.resolve('crypto-browserify'),
|
|
44
|
+
'util': require.resolve('util/'),
|
|
45
|
+
'events': require.resolve('events/'),
|
|
46
|
+
'url': require.resolve('url/'),
|
|
47
|
+
'buffer': require.resolve('buffer/'),
|
|
48
|
+
'stream': require.resolve('stream-browserify'),
|
|
49
|
+
'process/browser': require.resolve('process/browser'),
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
// since this webpack.config is invoked via webpack-dev-middleware in the process of the consuming server package (not this package)
|
|
53
|
+
// we need to let webpack know where to look for loaders like: file-loader and ts-loader
|
|
54
|
+
// it's ideal to keep those dependencies within this package instead of requiring them installed by the consumer
|
|
55
|
+
resolveLoader: {
|
|
56
|
+
modules: [path.resolve(__dirname, '../node_modules'), 'node_modules'],
|
|
57
|
+
},
|
|
58
|
+
module: {
|
|
59
|
+
rules: [
|
|
60
|
+
webpack5esmInteropRule,
|
|
61
|
+
// all files with a '.ts' or '.tsx' extension will be handled by 'ts-loader'
|
|
62
|
+
// this will only transpile (to integrate with babel via ReactRefreshTypeScript); rely on the ide for compiler checks
|
|
63
|
+
{
|
|
64
|
+
test: /\.[jt]sx?$/,
|
|
65
|
+
exclude: /node_modules/,
|
|
66
|
+
use: [
|
|
67
|
+
{
|
|
68
|
+
loader: require.resolve('ts-loader'),
|
|
69
|
+
options: {
|
|
70
|
+
getCustomTransformers: () => ({
|
|
71
|
+
before: [ReactRefreshTypeScript()],
|
|
72
|
+
}),
|
|
73
|
+
transpileOnly: true,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
// image files
|
|
79
|
+
{
|
|
80
|
+
test: /\.(png|jpe?g|gif|svg)$/i,
|
|
81
|
+
use: [
|
|
82
|
+
{
|
|
83
|
+
loader: 'file-loader',
|
|
84
|
+
options: {
|
|
85
|
+
name: '[path][name].[ext]', // This line will keep the original path and filename
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
optimization: {
|
|
93
|
+
splitChunks: {
|
|
94
|
+
cacheGroups: {
|
|
95
|
+
vendor: {
|
|
96
|
+
test: /[\\/]node_modules[\\/]/, // Matches node_modules folder
|
|
97
|
+
name: 'vendor',
|
|
98
|
+
chunks: 'all',
|
|
99
|
+
priority: -10
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
plugins: [
|
|
105
|
+
new webpack.ProvidePlugin({
|
|
106
|
+
process: 'process/browser',
|
|
107
|
+
Buffer: ['buffer', 'Buffer'],
|
|
108
|
+
}),
|
|
109
|
+
new webpack.HotModuleReplacementPlugin(),
|
|
110
|
+
new ReactRefreshWebpackPlugin(),
|
|
111
|
+
],
|
|
112
|
+
};
|