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