Initial Commit
This commit is contained in:
9
.dockerignore
Normal file
9
.dockerignore
Normal file
@@ -0,0 +1,9 @@
|
||||
*
|
||||
!src/*
|
||||
!public/*
|
||||
!views/*
|
||||
|
||||
!bio-proxy-configuration-for-docker/*
|
||||
!package.json
|
||||
!package-lock.json
|
||||
!tsconfig.json
|
||||
2
.eslintignore
Normal file
2
.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
out
|
||||
232
.gitignore
vendored
Normal file
232
.gitignore
vendored
Normal file
@@ -0,0 +1,232 @@
|
||||
tmp/*
|
||||
!tmp/redis-data
|
||||
tmp/redis-data/*
|
||||
!tmp/redis-data/.gitkeep
|
||||
!tmp/default-external.conf
|
||||
bundle.js
|
||||
*.map
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/webstorm,node,sass
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=webstorm,node,sass
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
.env*.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
### Sass ###
|
||||
.sass-cache/
|
||||
*.css.map
|
||||
*.sass.map
|
||||
*.scss.map
|
||||
|
||||
### WebStorm ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### WebStorm Patch ###
|
||||
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
|
||||
|
||||
# *.iml
|
||||
# modules.xml
|
||||
# .idea/misc.xml
|
||||
# *.ipr
|
||||
|
||||
# Sonarlint plugin
|
||||
# https://plugins.jetbrains.com/plugin/7973-sonarlint
|
||||
.idea/**/sonarlint/
|
||||
|
||||
# SonarQube Plugin
|
||||
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
|
||||
.idea/**/sonarIssues.xml
|
||||
|
||||
# Markdown Navigator plugin
|
||||
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
|
||||
.idea/**/markdown-navigator.xml
|
||||
.idea/**/markdown-navigator-enh.xml
|
||||
.idea/**/markdown-navigator/
|
||||
|
||||
# Cache file creation bug
|
||||
# See https://youtrack.jetbrains.com/issue/JBR-2257
|
||||
.idea/$CACHE_FILE$
|
||||
|
||||
# CodeStream plugin
|
||||
# https://plugins.jetbrains.com/plugin/12206-codestream
|
||||
.idea/codestream.xml
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/webstorm,node,sass
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "bio-proxy-configuration-for-docker"]
|
||||
path = bio-proxy-configuration-for-docker
|
||||
url = https://git.biotronik.int/scm/coe-bs-website/bio-proxy-configuration-for-docker.git
|
||||
28
.idea/codeStyles/Project.xml
generated
Normal file
28
.idea/codeStyles/Project.xml
generated
Normal file
@@ -0,0 +1,28 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<DBN-PSQL>
|
||||
<case-options enabled="true">
|
||||
<option name="KEYWORD_CASE" value="lower" />
|
||||
<option name="FUNCTION_CASE" value="lower" />
|
||||
<option name="PARAMETER_CASE" value="lower" />
|
||||
<option name="DATATYPE_CASE" value="lower" />
|
||||
<option name="OBJECT_CASE" value="preserve" />
|
||||
</case-options>
|
||||
<formatting-settings enabled="false" />
|
||||
</DBN-PSQL>
|
||||
<DBN-SQL>
|
||||
<case-options enabled="true">
|
||||
<option name="KEYWORD_CASE" value="lower" />
|
||||
<option name="FUNCTION_CASE" value="lower" />
|
||||
<option name="PARAMETER_CASE" value="lower" />
|
||||
<option name="DATATYPE_CASE" value="lower" />
|
||||
<option name="OBJECT_CASE" value="preserve" />
|
||||
</case-options>
|
||||
<formatting-settings enabled="false">
|
||||
<option name="STATEMENT_SPACING" value="one_line" />
|
||||
<option name="CLAUSE_CHOP_DOWN" value="chop_down_if_statement_long" />
|
||||
<option name="ITERATION_ELEMENTS_WRAPPING" value="chop_down_if_not_single" />
|
||||
</formatting-settings>
|
||||
</DBN-SQL>
|
||||
</code_scheme>
|
||||
</component>
|
||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||
</state>
|
||||
</component>
|
||||
6
.idea/compiler.xml
generated
Normal file
6
.idea/compiler.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="TypeScriptCompiler">
|
||||
<option name="showAllErrors" value="true" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
6
.idea/jsLibraryMappings.xml
generated
Normal file
6
.idea/jsLibraryMappings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptLibraryMappings">
|
||||
<includedPredefinedLibrary name="Node.js Core" />
|
||||
</component>
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/template-nodejs-express.iml" filepath="$PROJECT_DIR$/.idea/template-nodejs-express.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
12
.idea/template-nodejs-express.iml
generated
Normal file
12
.idea/template-nodejs-express.iml
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
7
.idea/vcs.xml
generated
Normal file
7
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/bio-proxy-configuration-for-docker" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
39
Dockerfile
Normal file
39
Dockerfile
Normal file
@@ -0,0 +1,39 @@
|
||||
FROM node:12-alpine
|
||||
|
||||
# set app user
|
||||
RUN addgroup --gid 800 appuser && \
|
||||
adduser --uid 800 --ingroup appuser --disabled-password --gecos "" appuser && \
|
||||
mkdir /app && \
|
||||
chown appuser:appuser /app
|
||||
USER appuser
|
||||
WORKDIR /app
|
||||
|
||||
# add proxy configuration
|
||||
USER root
|
||||
COPY ./bio-proxy-configuration-for-docker /tmp/bio-data
|
||||
ARG add_proxy
|
||||
RUN if [ -n "$add_proxy" ] ; then sed -i 's/\r$//g' /tmp/bio-data/alpine-based.sh && \
|
||||
chmod a+x /tmp/bio-data/alpine-based.sh && \
|
||||
/tmp/bio-data/alpine-based.sh && \
|
||||
rm -r /tmp/bio-data ; fi
|
||||
USER appuser
|
||||
|
||||
# copy data
|
||||
COPY . .
|
||||
|
||||
# install
|
||||
RUN npm run install-prod
|
||||
ENV NODE_ENV production
|
||||
|
||||
# cleanup
|
||||
USER root
|
||||
RUN rm -r ./src ./public/js-source ./bio-proxy-configuration-for-docker && \
|
||||
npm cache clean --force
|
||||
USER appuser
|
||||
|
||||
# healthcheck
|
||||
HEALTHCHECK --interval=10s --timeout=2s --start-period=15s \
|
||||
CMD node ./out/healthcheck.js
|
||||
|
||||
# set command
|
||||
CMD npm run production
|
||||
1
bio-proxy-configuration-for-docker
Submodule
1
bio-proxy-configuration-for-docker
Submodule
Submodule bio-proxy-configuration-for-docker added at 42fe10c84a
49
docker-compose.debug.yml
Normal file
49
docker-compose.debug.yml
Normal file
@@ -0,0 +1,49 @@
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
command: redis-server --appendonly yes
|
||||
volumes:
|
||||
- ./tmp/redis-data:/data
|
||||
ports:
|
||||
- 6379:6379
|
||||
networks:
|
||||
- backend
|
||||
proxy:
|
||||
image: docker-registry.biotronik.int/web/auth-proxy
|
||||
depends_on:
|
||||
- redis
|
||||
environment:
|
||||
- "PROXY_USERINFO_SECRET=3d513168-c92d-4f57-8c78-8fb2efad8a34"
|
||||
- "PROXY_TARGET_URI=http://host.docker.internal:3000"
|
||||
- "HOST=0.0.0.0"
|
||||
- "COOKIE_SECRET=05e8cc4b-f95f-4a70-b4a1-b22ce295348d"
|
||||
- "WELLKNOWN_CONFIG_URI=https://nodejs1-2.biotronik.int/auth/realms/CoE-BS/.well-known/openid-configuration"
|
||||
- "CLIENT_ID=demo"
|
||||
- "CLIENT_SECRET=fb49b346-c515-4680-adb1-beee0bd5b66e"
|
||||
- "CLIENT_SCOPE=openid email profile roles groups"
|
||||
- "NODE_ENV=debug"
|
||||
- "SSL_VERIFY=false"
|
||||
- "EXT_RESOURCE_URI=http://localhost"
|
||||
- "REDIS_URL=redis://redis:6379"
|
||||
- "NO_PROXY=redis:6379"
|
||||
ports:
|
||||
- 3001:3000
|
||||
networks:
|
||||
- frontend
|
||||
- backend
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
depends_on:
|
||||
- proxy
|
||||
volumes:
|
||||
- ./tmp/default-external.conf:/etc/nginx/conf.d/default.conf
|
||||
ports:
|
||||
- 80:80
|
||||
networks:
|
||||
- frontend
|
||||
|
||||
networks:
|
||||
backend:
|
||||
frontend:
|
||||
61
docker-compose.yml
Normal file
61
docker-compose.yml
Normal file
@@ -0,0 +1,61 @@
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
- add_proxy=1
|
||||
environment:
|
||||
- "REDIS_URL=redis://redis:6379"
|
||||
- "SESSION_SECRET=replace with random"
|
||||
- "USERINFO_HEADER=X-Userinfo-Token"
|
||||
- "AUTH_PROXY_URL=http://proxy:3000/oauth"
|
||||
- "NO_PROXY=redis:6379"
|
||||
depends_on:
|
||||
- redis
|
||||
networks:
|
||||
- backend
|
||||
redis:
|
||||
image: redis
|
||||
command: redis-server --appendonly yes
|
||||
volumes:
|
||||
- ./tmp/redis-data:/data
|
||||
networks:
|
||||
- backend
|
||||
proxy:
|
||||
image: docker-registry.biotronik.int/web/auth-proxy
|
||||
depends_on:
|
||||
- app
|
||||
- redis
|
||||
environment:
|
||||
- "PROXY_USERINFO_SECRET=3d513168-c92d-4f57-8c78-8fb2efad8a34"
|
||||
- "PROXY_TARGET_URI=http://app:3000"
|
||||
- "HOST=0.0.0.0"
|
||||
- "COOKIE_SECRET=05e8cc4b-f95f-4a70-b4a1-b22ce295348d"
|
||||
- "WELLKNOWN_CONFIG_URI=https://nodejs1-2.biotronik.int/auth/realms/CoE-BS/.well-known/openid-configuration"
|
||||
- "CLIENT_ID=demo"
|
||||
- "CLIENT_SECRET=fb49b346-c515-4680-adb1-beee0bd5b66e"
|
||||
- "CLIENT_SCOPE=openid email profile roles groups"
|
||||
- "NODE_ENV=debug"
|
||||
- "SSL_VERIFY=false"
|
||||
- "EXT_RESOURCE_URI=http://localhost"
|
||||
- "REDIS_URL=redis://redis:6379"
|
||||
- "NO_PROXY=redis:6379"
|
||||
networks:
|
||||
- backend
|
||||
- frontend
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
depends_on:
|
||||
- proxy
|
||||
volumes:
|
||||
- ./tmp/default-external.conf:/etc/nginx/conf.d/default.conf
|
||||
ports:
|
||||
- 80:80
|
||||
networks:
|
||||
- frontend
|
||||
|
||||
networks:
|
||||
backend:
|
||||
frontend:
|
||||
3752
package-lock.json
generated
Normal file
3752
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
50
package.json
Normal file
50
package.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "template-nodejs-express",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "eslint . --ext .ts",
|
||||
"lint-fix": "eslint . --ext .ts --fix",
|
||||
"debug": "tsc-watch --onSuccess \"node --enable-source-maps --use-openssl-ca --unhandled-rejections=strict ./out/index\"",
|
||||
"build": "tsc",
|
||||
"production": "node --use-openssl-ca --unhandled-rejections=strict ./out/index",
|
||||
"install-debug": "npm install && npm run build",
|
||||
"install-prod": "npm install --only=dev && npm install --only=prod && npm run build && rm -r node_modules && npm install --only=prod",
|
||||
"install-prod-win": "npm install --only=dev && npm install --only=prod && npm run build && rmdir /Q/S node_modules && npm install --only=prod"
|
||||
},
|
||||
"dependencies": {
|
||||
"connect-redis": "^5.0.0",
|
||||
"cookie-parser": "~1.4.4",
|
||||
"env-var": "^6.3.0",
|
||||
"express": "~4.16.1",
|
||||
"express-session": "^1.17.1",
|
||||
"http-errors": "~1.6.3",
|
||||
"json-prune": "^1.1.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-sass-middleware": "0.11.0",
|
||||
"proper-url-join": "^2.1.1",
|
||||
"pug": "^3.0.0",
|
||||
"redis": "^3.0.2",
|
||||
"uuid": "^8.3.1",
|
||||
"winston": "^3.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/connect-redis": "0.0.15",
|
||||
"@types/express": "^4.17.8",
|
||||
"@types/express-session": "^1.17.1",
|
||||
"@types/http-errors": "^1.8.0",
|
||||
"@types/node": "^14.14.7",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/node-sass-middleware": "0.0.31",
|
||||
"@types/proper-url-join": "^2.0.0",
|
||||
"@types/redis": "^2.8.28",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.7.0",
|
||||
"@typescript-eslint/parser": "^4.7.0",
|
||||
"eslint": "^7.13.0",
|
||||
"eslint-plugin-no-null": "^1.0.2",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"tsc-watch": "^4.2.9",
|
||||
"typescript": "^4.0.5"
|
||||
}
|
||||
}
|
||||
39
public/js-source/.eslintrc.js
Normal file
39
public/js-source/.eslintrc.js
Normal file
@@ -0,0 +1,39 @@
|
||||
// eslint-disable-next-line no-undef
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: [
|
||||
'@typescript-eslint',
|
||||
'no-null',
|
||||
],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
],
|
||||
rules: {
|
||||
"no-console": "error",
|
||||
"max-len": ["error", {"code": 128}],
|
||||
"no-process-env": "error",
|
||||
"no-process-exit": "error",
|
||||
"no-null/no-null": "error",
|
||||
"no-useless-return": "error",
|
||||
"prefer-arrow-callback": "warn",
|
||||
"object-curly-spacing": "error",
|
||||
"consistent-return": "error",
|
||||
"@typescript-eslint/explicit-function-return-type": [
|
||||
"error", {
|
||||
"allowExpressions": true,
|
||||
},
|
||||
],
|
||||
"no-void": "error",
|
||||
"comma-spacing": "error",
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
|
||||
"no-restricted-imports": ["error",
|
||||
"assert", "buffer", "child_process", "cluster", "crypto", "dgram", "dns", "domain", "events", "freelist",
|
||||
"fs", "http", "https", "module", "net", "os", "path", "punycode", "querystring", "readline", "repl",
|
||||
"smalloc", "stream", "string_decoder", "sys", "timers", "tls", "tracing", "tty", "url", "util", "vm", "zlib",
|
||||
],
|
||||
},
|
||||
};
|
||||
7
public/js-source/src/SomeModule.ts
Normal file
7
public/js-source/src/SomeModule.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import {getUserInfo} from './utils';
|
||||
|
||||
export async function getUserName(): Promise<string> {
|
||||
const info = await getUserInfo();
|
||||
return info?.name ?? "No name found!";
|
||||
}
|
||||
|
||||
12
public/js-source/src/index.ts
Normal file
12
public/js-source/src/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// Export setConfig so that the external url can set set automatically
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export {setConfig} from './utils';
|
||||
|
||||
import {getUserName} from './SomeModule';
|
||||
|
||||
getUserName().then((name) => {
|
||||
const node = document.createElement('span');
|
||||
node.innerText = `This user name is fetched with Javascript: ${name}`;
|
||||
document.getElementsByTagName("body")[0].appendChild(node);
|
||||
});
|
||||
78
public/js-source/src/resolvable.ts
Normal file
78
public/js-source/src/resolvable.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
enum ResolvableState {
|
||||
WAITING,
|
||||
PENDING,
|
||||
ERROR,
|
||||
DONE
|
||||
}
|
||||
|
||||
class FetchOnce<T> {
|
||||
protected data: T|undefined;
|
||||
protected error: unknown|undefined;
|
||||
protected state: ResolvableState = ResolvableState.WAITING;
|
||||
protected pendings: [(res: Promise<T>|T) => void, (reason: unknown) => void][] = [];
|
||||
|
||||
constructor(protected fetchMethod?: () => Promise<T>) { }
|
||||
|
||||
public resolve(): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
switch (this.state) {
|
||||
case ResolvableState.WAITING:
|
||||
this.state = ResolvableState.PENDING;
|
||||
this.pendings.push([resolve, reject]);
|
||||
if (this.fetchMethod) this.parsePromise(this.fetchMethod());
|
||||
break;
|
||||
case ResolvableState.PENDING:
|
||||
this.pendings.push([resolve, reject]);
|
||||
break;
|
||||
case ResolvableState.DONE:
|
||||
resolve(this.data);
|
||||
break;
|
||||
case ResolvableState.ERROR:
|
||||
reject(this.error);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected isFinished(): boolean {
|
||||
return this.state === ResolvableState.DONE || this.state === ResolvableState.ERROR;
|
||||
}
|
||||
|
||||
protected parsePromise(promise: Promise<T>): void {
|
||||
promise.then((data) => {
|
||||
this.data = data;
|
||||
this.state = ResolvableState.DONE;
|
||||
this.pendings.forEach(pending => pending[0](data));
|
||||
}).catch(err => {
|
||||
this.error = err;
|
||||
this.state = ResolvableState.ERROR;
|
||||
this.pendings.forEach(pending => pending[1](err));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class Resolvable<T> extends FetchOnce<T> {
|
||||
constructor(fetchMethod: () => Promise<T>) {
|
||||
super(fetchMethod);
|
||||
}
|
||||
}
|
||||
|
||||
export class WaitForSync<T> extends FetchOnce<T> {
|
||||
protected state: ResolvableState = ResolvableState.PENDING;
|
||||
|
||||
constructor() {
|
||||
super(undefined);
|
||||
}
|
||||
|
||||
public setData(data: T): void {
|
||||
if (!this.isFinished()) {
|
||||
this.parsePromise((async () => data)());
|
||||
}
|
||||
}
|
||||
|
||||
public setError(error: unknown): void {
|
||||
if (!this.isFinished()) {
|
||||
this.parsePromise((async () => { throw error; })());
|
||||
}
|
||||
}
|
||||
}
|
||||
10
public/js-source/src/types/userinfo.d.ts
vendored
Normal file
10
public/js-source/src/types/userinfo.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
type UserInfo = {
|
||||
email: string,
|
||||
email_verified: boolean,
|
||||
family_name: string,
|
||||
given_name: string,
|
||||
groups: string[],
|
||||
name: string,
|
||||
preferred_username: string,
|
||||
sub: string,
|
||||
};
|
||||
23
public/js-source/src/utils.ts
Normal file
23
public/js-source/src/utils.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {WaitForSync} from './resolvable';
|
||||
|
||||
export interface ClientConfig {
|
||||
EXTERNAL_BASE_URL: string
|
||||
}
|
||||
|
||||
let config: ClientConfig|undefined;
|
||||
const configWaiter = new WaitForSync<ClientConfig>();
|
||||
|
||||
export function setConfig(clientConfig: ClientConfig): void {
|
||||
config = clientConfig;
|
||||
configWaiter.setData(config);
|
||||
}
|
||||
|
||||
export function getConfig(): Promise<ClientConfig> {
|
||||
return configWaiter.resolve();
|
||||
}
|
||||
|
||||
export async function getUserInfo(): Promise<UserInfo|undefined> {
|
||||
const getBaseUrl = await getConfig();
|
||||
const res = await fetch(getBaseUrl.EXTERNAL_BASE_URL + "/api/user");
|
||||
return res.json();
|
||||
}
|
||||
23
public/js-source/tsconfig.json
Normal file
23
public/js-source/tsconfig.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "amd",
|
||||
"lib": ["es5", "es6", "dom"],
|
||||
"removeComments": true,
|
||||
"preserveConstEnums": true,
|
||||
"outFile": "../js/bundle.js",
|
||||
"target": "es5",
|
||||
"sourceMap": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitReturns": true,
|
||||
"strict": true,
|
||||
"sourceRoot": "./src",
|
||||
"rootDir": ".",
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": "."
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
]
|
||||
}
|
||||
5
public/js/require-2.3.6.min.js
vendored
Normal file
5
public/js/require-2.3.6.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
8
public/styles/style.css
Normal file
8
public/styles/style.css
Normal file
@@ -0,0 +1,8 @@
|
||||
body {
|
||||
padding: 50px;
|
||||
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; }
|
||||
|
||||
a {
|
||||
color: #00B7FF; }
|
||||
|
||||
/*# sourceMappingURL=style.css.map */
|
||||
6
public/styles/style.sass
Normal file
6
public/styles/style.sass
Normal file
@@ -0,0 +1,6 @@
|
||||
body
|
||||
padding: 50px
|
||||
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif
|
||||
|
||||
a
|
||||
color: #00B7FF
|
||||
45
src/.eslintrc.js
Normal file
45
src/.eslintrc.js
Normal file
@@ -0,0 +1,45 @@
|
||||
// eslint-disable-next-line no-undef
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: [
|
||||
'@typescript-eslint',
|
||||
'no-null',
|
||||
'promise',
|
||||
],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
],
|
||||
rules: {
|
||||
"no-console": "error",
|
||||
"max-len": ["error", {"code": 128}],
|
||||
"no-process-env": "error",
|
||||
"no-process-exit": "error",
|
||||
"no-null/no-null": "error",
|
||||
"no-useless-return": "error",
|
||||
"prefer-arrow-callback": "warn",
|
||||
"object-curly-spacing": "error",
|
||||
"consistent-return": "error",
|
||||
"@typescript-eslint/explicit-function-return-type": [
|
||||
"error", {
|
||||
"allowExpressions": true,
|
||||
},
|
||||
],
|
||||
"no-void": "error",
|
||||
"comma-spacing": "error",
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"promise/no-return-wrap": "error",
|
||||
"promise/param-names": "error",
|
||||
"promise/catch-or-return": "error",
|
||||
"promise/no-native": "off",
|
||||
"promise/no-nesting": "warn",
|
||||
"promise/no-promise-in-callback": "warn",
|
||||
"promise/no-callback-in-promise": "warn",
|
||||
"promise/avoid-new": "warn",
|
||||
"promise/no-new-statics": "error",
|
||||
"promise/no-return-in-finally": "warn",
|
||||
"promise/valid-params": "warn",
|
||||
},
|
||||
};
|
||||
75
src/app.ts
Normal file
75
src/app.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import * as createError from 'http-errors';
|
||||
import * as express from 'express';
|
||||
import {NextFunction, Request, Response} from 'express';
|
||||
import * as path from 'path';
|
||||
import * as redisStore from 'connect-redis';
|
||||
import * as session from 'express-session';
|
||||
import * as sassMiddleware from 'node-sass-middleware';
|
||||
import indexRouter from './routes';
|
||||
import {HttpLogger, Redis, Config, setupAuthProxy} from './utils';
|
||||
import {Store} from 'express-session';
|
||||
|
||||
|
||||
export const app = express();
|
||||
|
||||
// view engine setup
|
||||
app.set('views', path.join(__dirname, '../views'));
|
||||
app.set('view engine', 'pug');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// http logger
|
||||
app.use(HttpLogger);
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({extended: false}));
|
||||
|
||||
// auth proxy middleware
|
||||
app.use(setupAuthProxy);
|
||||
|
||||
// session
|
||||
let sessionStore: Store|undefined = undefined;
|
||||
if (Redis.client) {
|
||||
const RedisStore = redisStore(session);
|
||||
sessionStore = new RedisStore({client: Redis.client});
|
||||
}
|
||||
app.use(session({
|
||||
store: sessionStore,
|
||||
secret: Config.SESSION_SECRET,
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
cookie: {secure: false},
|
||||
}));
|
||||
|
||||
// static config
|
||||
router.use(sassMiddleware({
|
||||
src: path.join(__dirname, '../public'),
|
||||
dest: path.join(__dirname, '../public'),
|
||||
indentedSyntax: true, // true = .sass and false = .scss
|
||||
sourceMap: true,
|
||||
}));
|
||||
router.use(express.static(path.join(__dirname, '../public')));
|
||||
|
||||
router.use(indexRouter);
|
||||
app.use(Config.BASE_PATH, router);
|
||||
|
||||
// catch 404 and forward to error handler
|
||||
app.use((req, res, next) => {
|
||||
next(createError(404));
|
||||
});
|
||||
|
||||
// error handler
|
||||
app.use((err: Error&{status?: number}, req: Request, res: Response, next: NextFunction) => {
|
||||
if (res.headersSent) {
|
||||
return next(err);
|
||||
}
|
||||
// set locals, only providing error in development
|
||||
res.locals.message = err.message;
|
||||
res.locals.error = !Config.isProduction ? err : {};
|
||||
|
||||
// render the error page
|
||||
res.status(err.status || 500);
|
||||
res.render('error');
|
||||
return undefined;
|
||||
});
|
||||
|
||||
|
||||
22
src/healthcheck.ts
Normal file
22
src/healthcheck.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/* eslint-disable no-process-exit,no-console */
|
||||
import * as http from "http";
|
||||
import urlJoin from 'proper-url-join';
|
||||
import {Config} from './utils';
|
||||
|
||||
const options = {
|
||||
host: Config.HOSTNAME,
|
||||
port: Config.PORT,
|
||||
path: urlJoin(Config.BASE_PATH, '/health'),
|
||||
timeout: 2000,
|
||||
};
|
||||
|
||||
const healthCheck = http.request(options, (res) => {
|
||||
process.exit(res.statusCode == 200 ? 0 : 1);
|
||||
});
|
||||
|
||||
healthCheck.on('error', (err) => {
|
||||
console.error('HEALTHCHECK ERROR', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
healthCheck.end();
|
||||
42
src/index.ts
Normal file
42
src/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-disable no-process-exit */
|
||||
|
||||
import * as http from 'http';
|
||||
import {app} from './app';
|
||||
import {HttpError} from 'http-errors';
|
||||
import {Config, Logger} from './utils';
|
||||
|
||||
app.set('port', Config.PORT);
|
||||
const server = http.createServer(app);
|
||||
|
||||
server.listen(Config.PORT);
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
|
||||
function onError(error: HttpError): void {
|
||||
if (error.syscall !== 'listen') {
|
||||
Logger.error(error);
|
||||
}
|
||||
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
Logger.error(Config.PORT + ' requires elevated privileges');
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
Logger.error(Config.PORT + ' is already in use');
|
||||
process.exit(2);
|
||||
break;
|
||||
default:
|
||||
Logger.error(error);
|
||||
process.exit(3);
|
||||
}
|
||||
}
|
||||
|
||||
function onListening(): void {
|
||||
const addr = server.address();
|
||||
const bind = typeof addr === 'string'
|
||||
? 'pipe ' + addr
|
||||
: 'port ' + (addr ?? {port: undefined}).port;
|
||||
Logger.debug('Listening on ' + bind);
|
||||
}
|
||||
10
src/routes/api/user.ts
Normal file
10
src/routes/api/user.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import * as express from 'express';
|
||||
|
||||
const userRouter = express.Router();
|
||||
|
||||
/* GET users listing. */
|
||||
userRouter.get('/', async (req, res) => {
|
||||
res.json(await req.getUserInfo() || {});
|
||||
});
|
||||
|
||||
export default userRouter;
|
||||
9
src/routes/healthcheck.ts
Normal file
9
src/routes/healthcheck.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as express from 'express';
|
||||
|
||||
const healthRouter = express.Router();
|
||||
|
||||
healthRouter.get('/', async (req, res) => {
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
export default healthRouter;
|
||||
20
src/routes/index.ts
Normal file
20
src/routes/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import * as express from 'express';
|
||||
import userRouter from './api/user';
|
||||
import healthRouter from './healthcheck';
|
||||
import {Config} from '../utils';
|
||||
|
||||
const router = express.Router();
|
||||
export default router;
|
||||
|
||||
router.use("/api/user", userRouter);
|
||||
router.use("/health", healthRouter);
|
||||
|
||||
/* GET home page. */
|
||||
router.get('/', async (req, res) => {
|
||||
const email = (await req.getUserInfo())?.email ?? "No email found!";
|
||||
res.render('index', {title: 'Express', email, externalUrl: Config.EXTERNAL_BASE_URL});
|
||||
});
|
||||
|
||||
router.get('/logout', (req, res) => {
|
||||
res.initLogout();
|
||||
});
|
||||
37
src/utils/auth-proxy.ts
Normal file
37
src/utils/auth-proxy.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import {Request, RequestHandler} from 'express';
|
||||
import {Config, Logger} from '.';
|
||||
import {Resolvable} from './helpers/resolvable';
|
||||
import fetch from 'node-fetch';
|
||||
import * as properUrlJoin from 'proper-url-join';
|
||||
const urlJoin = properUrlJoin as unknown as properUrlJoin.default;
|
||||
|
||||
export const setupAuthProxy: RequestHandler = (req: Request, res, next) => {
|
||||
const resolvable = new Resolvable<UserInfo|undefined>(async () => {
|
||||
if (!Config.USERINFO_HEADER) {
|
||||
return undefined;
|
||||
}
|
||||
const token = req.header(Config.USERINFO_HEADER);
|
||||
const url = Config.AUTH_PROXY_USERINFO_URL || Config.AUTH_PROXY_URL && urlJoin(Config.AUTH_PROXY_URL, "userinfo");
|
||||
if (token === undefined || url === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const res = await fetch(url, {headers: [[Config.USERINFO_HEADER, token]]});
|
||||
return await res.json();
|
||||
} catch (e) {
|
||||
Logger.warn(e);
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
req.getUserInfo = () => resolvable.resolve();
|
||||
res.initLogout = function() {
|
||||
const url = Config.AUTH_PROXY_INIT_LOGOUT_URL || Config.AUTH_PROXY_URL && urlJoin(Config.AUTH_PROXY_URL, "init-logout");
|
||||
if (url === undefined) {
|
||||
return false;
|
||||
}
|
||||
this.redirect(307, url);
|
||||
return true;
|
||||
}
|
||||
next();
|
||||
}
|
||||
40
src/utils/config.ts
Normal file
40
src/utils/config.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import * as env from 'env-var';
|
||||
import * as properUrlJoin from 'proper-url-join';
|
||||
const urlJoin = properUrlJoin as unknown as properUrlJoin.default;
|
||||
|
||||
const NODE_ENV = env.get('NODE_ENV').default("development").asString();
|
||||
const isProduction = NODE_ENV === 'production';
|
||||
|
||||
const envs = {
|
||||
NODE_ENV,
|
||||
// port of the server
|
||||
PORT: env.get('PORT').default('3000').asPortNumber(),
|
||||
// hostname of the server ('0.0.0.0' listens on all network interfaces)
|
||||
HOSTNAME: env.get('HOSTNAME').default('0.0.0.0').asString(),
|
||||
// base path
|
||||
BASE_PATH: env.get('BASE_PATH').default('/').asString(),
|
||||
// external base url
|
||||
EXTERNAL_BASE_URL: env.get('EXTERNAL_BASE_URL').asString(),
|
||||
|
||||
// url of redis session store
|
||||
REDIS_URL: env.get('REDIS_URL').required(isProduction).asString() || undefined,
|
||||
// cookie secret for the session id
|
||||
SESSION_SECRET: env.get('REDIS_URL').required(isProduction).default('keyboard cat').asString(),
|
||||
|
||||
// header where user info token is stored to request auth proxy
|
||||
USERINFO_HEADER: env.get('USERINFO_HEADER').asString() || undefined,
|
||||
// base url to init a logout or request user info
|
||||
AUTH_PROXY_URL: env.get('AUTH_PROXY_URL').asString() || undefined,
|
||||
// override base url to request user info
|
||||
AUTH_PROXY_USERINFO_URL: env.get('AUTH_PROXY_USERINFO_URL').asString() || undefined,
|
||||
// override base url to init a logout
|
||||
AUTH_PROXY_INIT_LOGOUT_URL: env.get('AUTH_PROXY_INIT_LOGOUT_URL').asString() || undefined,
|
||||
|
||||
};
|
||||
|
||||
|
||||
export const Config = {
|
||||
...envs,
|
||||
isProduction,
|
||||
EXTERNAL_BASE_URL: envs.EXTERNAL_BASE_URL || urlJoin(`http://${envs.HOSTNAME}${envs.PORT !== 80 ? `:${envs.PORT}` : ""}`),
|
||||
}
|
||||
79
src/utils/helpers/resolvable.ts
Normal file
79
src/utils/helpers/resolvable.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
enum ResolvableState {
|
||||
WAITING,
|
||||
PENDING,
|
||||
ERROR,
|
||||
DONE
|
||||
}
|
||||
|
||||
class FetchOnce<T> {
|
||||
protected data: T|undefined;
|
||||
protected error: unknown|undefined;
|
||||
protected state: ResolvableState = ResolvableState.WAITING;
|
||||
protected pendings: [(res: Promise<T>|T) => void, (reason: unknown) => void][] = [];
|
||||
|
||||
constructor(protected fetchMethod?: () => Promise<T>) { }
|
||||
|
||||
public resolve(): Promise<T> {
|
||||
// eslint-disable-next-line promise/avoid-new
|
||||
return new Promise((resolve, reject) => {
|
||||
switch (this.state) {
|
||||
case ResolvableState.WAITING:
|
||||
this.state = ResolvableState.PENDING;
|
||||
this.pendings.push([resolve, reject]);
|
||||
if (this.fetchMethod) this.parsePromise(this.fetchMethod());
|
||||
break;
|
||||
case ResolvableState.PENDING:
|
||||
this.pendings.push([resolve, reject]);
|
||||
break;
|
||||
case ResolvableState.DONE:
|
||||
resolve(this.data);
|
||||
break;
|
||||
case ResolvableState.ERROR:
|
||||
reject(this.error);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected isFinished(): boolean {
|
||||
return this.state === ResolvableState.DONE || this.state === ResolvableState.ERROR;
|
||||
}
|
||||
|
||||
protected parsePromise(promise: Promise<T>): void {
|
||||
promise.then((data) => {
|
||||
this.data = data;
|
||||
this.state = ResolvableState.DONE;
|
||||
this.pendings.forEach(pending => pending[0](data));
|
||||
}).catch(err => {
|
||||
this.error = err;
|
||||
this.state = ResolvableState.ERROR;
|
||||
this.pendings.forEach(pending => pending[1](err));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class Resolvable<T> extends FetchOnce<T> {
|
||||
constructor(fetchMethod: () => Promise<T>) {
|
||||
super(fetchMethod);
|
||||
}
|
||||
}
|
||||
|
||||
export class WaitForSync<T> extends FetchOnce<T> {
|
||||
protected state: ResolvableState = ResolvableState.PENDING;
|
||||
|
||||
constructor() {
|
||||
super(undefined);
|
||||
}
|
||||
|
||||
public setData(data: T): void {
|
||||
if (!this.isFinished()) {
|
||||
this.parsePromise((async () => data)());
|
||||
}
|
||||
}
|
||||
|
||||
public setError(error: unknown): void {
|
||||
if (!this.isFinished()) {
|
||||
this.parsePromise((async () => { throw error; })());
|
||||
}
|
||||
}
|
||||
}
|
||||
4
src/utils/index.ts
Normal file
4
src/utils/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export {Config} from './config';
|
||||
export {Redis} from './redis';
|
||||
export {Logger, HttpLogger} from './logging';
|
||||
export {setupAuthProxy} from './auth-proxy';
|
||||
52
src/utils/logging.ts
Normal file
52
src/utils/logging.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import * as winston from 'winston';
|
||||
import {LeveledLogMethod} from 'winston';
|
||||
import prune = require('json-prune');
|
||||
import {RequestHandler} from 'express';
|
||||
import * as colors from 'colors';
|
||||
import {Config} from '.';
|
||||
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: Config.isProduction ? "info" : "silly",
|
||||
format: winston.format.json(),
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
format: winston.format.simple(),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const levels = ["error", "warn", "info", "http", "verbose", "debug", "silly"] as const;
|
||||
type LogLevels = typeof levels[number];
|
||||
const wrapper = (original: LeveledLogMethod) => {
|
||||
return (...args: unknown[]) => {
|
||||
return original(args.map((obj) => typeof obj === "string" ? obj : prune(obj)).join(" "));
|
||||
};
|
||||
};
|
||||
|
||||
export const Logger = {
|
||||
} as {[level in LogLevels]: ReturnType<typeof wrapper>};
|
||||
|
||||
for (const level of levels) {
|
||||
Logger[level] = wrapper(logger[level]);
|
||||
}
|
||||
|
||||
export const HttpLogger: RequestHandler = (req, res, next) => {
|
||||
const start = Date.now();
|
||||
type Callback = (() => void)|undefined;
|
||||
const end = res.end;
|
||||
res.end = function(...args: [Callback] & [unknown, Callback] & [unknown, BufferEncoding, Callback]) {
|
||||
const statusCode = res.statusCode;
|
||||
const colorFunction = statusCode >= 500 ? colors.red
|
||||
: statusCode >= 400 ? colors.yellow
|
||||
: statusCode >= 300 ? colors.cyan
|
||||
: statusCode >= 200 ? colors.green
|
||||
: colors.gray;
|
||||
const status = colorFunction(res.statusCode.toString(10));
|
||||
const method = req.method.toUpperCase().padEnd(6, " ");
|
||||
const responseTime = (Date.now()-start).toString(10).padStart(3, " ");
|
||||
Logger.http(`${status} ${method} ${responseTime}ms ${req.path}`);
|
||||
end.apply(res, args);
|
||||
};
|
||||
next();
|
||||
}
|
||||
36
src/utils/redis.ts
Normal file
36
src/utils/redis.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import * as redis from 'redis';
|
||||
import {Config} from '.';
|
||||
import {promisify} from 'util';
|
||||
|
||||
const redisClient = Config.REDIS_URL && redis.createClient({url: Config.REDIS_URL});
|
||||
const inMemory: {[key: string]: string} = {};
|
||||
|
||||
type RedisType = {
|
||||
client: redis.RedisClient | undefined,
|
||||
get: (key: string) => Promise<string | undefined>,
|
||||
set: (key: string, value: string) => Promise<void>,
|
||||
del: (...keys: string[]) => Promise<void>
|
||||
}
|
||||
|
||||
export const Redis: RedisType = redisClient ? {
|
||||
client: redisClient,
|
||||
get: (async (key: string) => {
|
||||
const value = await promisify(redisClient.get)(key);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
return value === null ? undefined : value;
|
||||
}),
|
||||
set: promisify(redisClient.set),
|
||||
del: promisify(redisClient.del) as unknown as (...keys: string[]) => Promise<void>,
|
||||
} : {
|
||||
client: undefined,
|
||||
get: (async (key: string) => inMemory[key] || undefined),
|
||||
set: (async (key: string, value: string) => {
|
||||
inMemory[key] = value;
|
||||
}),
|
||||
del: (async (...keys: string[]) => {
|
||||
for (const key of keys) {
|
||||
delete inMemory[key];
|
||||
}
|
||||
}),
|
||||
};
|
||||
|
||||
9
src/utils/types/auth-proxy.d.ts
vendored
Normal file
9
src/utils/types/auth-proxy.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
declare namespace Express {
|
||||
interface Request {
|
||||
getUserInfo(): Promise<UserInfo|undefined>;
|
||||
}
|
||||
|
||||
interface Response {
|
||||
initLogout(): boolean;
|
||||
}
|
||||
}
|
||||
5
src/utils/types/json-prune.d.ts
vendored
Normal file
5
src/utils/types/json-prune.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
declare module "json-prune" {
|
||||
function prune(object: unknown): string;
|
||||
|
||||
export = prune;
|
||||
}
|
||||
10
src/utils/types/userinfo.d.ts
vendored
Normal file
10
src/utils/types/userinfo.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
type UserInfo = {
|
||||
email: string,
|
||||
email_verified: boolean,
|
||||
family_name: string,
|
||||
given_name: string,
|
||||
groups: string[],
|
||||
name: string,
|
||||
preferred_username: string,
|
||||
sub: string,
|
||||
};
|
||||
11
tmp/default-external.conf
Normal file
11
tmp/default-external.conf
Normal file
@@ -0,0 +1,11 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
access_log /var/log/nginx/reverse-access.log;
|
||||
error_log /var/log/nginx/reverse-error.log;
|
||||
|
||||
location / {
|
||||
proxy_pass http://proxy:3000/;
|
||||
}
|
||||
}
|
||||
0
tmp/redis-data/.gitkeep
Normal file
0
tmp/redis-data/.gitkeep
Normal file
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"sourceMap": true,
|
||||
"outDir": "out",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitReturns": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"public"
|
||||
]
|
||||
}
|
||||
6
views/error.pug
Normal file
6
views/error.pug
Normal file
@@ -0,0 +1,6 @@
|
||||
extends layout
|
||||
|
||||
block content
|
||||
h1= message
|
||||
h2= error.status
|
||||
pre #{error.stack}
|
||||
7
views/index.pug
Normal file
7
views/index.pug
Normal file
@@ -0,0 +1,7 @@
|
||||
extends layout
|
||||
|
||||
block content
|
||||
h1= title
|
||||
p Welcome to #{title}!
|
||||
a(href="/logout") Want so say goodbye?
|
||||
p This name is fetched on server-side: #{email}
|
||||
14
views/layout.pug
Normal file
14
views/layout.pug
Normal file
@@ -0,0 +1,14 @@
|
||||
doctype html
|
||||
html
|
||||
head
|
||||
title= title
|
||||
link(rel='stylesheet', href='/styles/style.css')
|
||||
body
|
||||
block content
|
||||
script(type='text/javascript', src='/js/require-2.3.6.min.js')
|
||||
script(type='text/javascript', src='/js/bundle.js')
|
||||
script.
|
||||
require(['src/index'], function (index) {
|
||||
console.log(index);
|
||||
index.setConfig({EXTERNAL_BASE_URL: '#{externalUrl}'});
|
||||
});
|
||||
Reference in New Issue
Block a user