diff --git a/.github/workflows/deploy-cr.yml b/.github/workflows/deploy-cr.yml index 8008bee..b9c8490 100644 --- a/.github/workflows/deploy-cr.yml +++ b/.github/workflows/deploy-cr.yml @@ -1,6 +1,24 @@ -# This is a basic workflow that is manually triggered +# This manually triggered workflow deploys front end application to a object store bucket # try to use as much shell scripting a possible # actions appear from default branch - https://github.community/t/workflow-files-only-picked-up-from-master/16129/2 +# +# Supported Container Registries: +# - [to test] Aliyun OSS +# - [backlog] AWS S3 +# - [backlog] Azure +# - [backlog] GCP Cloud Storage +# +# Setup the following secrets +# - CR_USERNAME +# - CR_PASSWORD +# Setup the following vars +# - CR_HOST +# - CR_NS +# - CR_IMAGENAME +# Specify the following during build +# - tag_gr +# - cr_env (dev, uat, prd) +# name: CR deployment on: @@ -23,6 +41,10 @@ on: description: 'path to dockerfile' default: '.' required: false + cr_env: + description: 'environment: dev, uat, stg, prd' + default: 'dev' + required: true env: CR_IMAGENAME: ${{ github.event.inputs.cr_imagename || vars.CR_IMAGENAME || github.event.repository.name }} CR_PASSWORD: ${{ secrets.CR_PASSWORD }} @@ -51,8 +73,8 @@ jobs: echo $CR_PASSWORD | docker login --username $CR_USERNAME --password-stdin $CR_HOST docker build -t $CR_HOST/$CR_NS/$CR_IMAGENAME:${{ github.event.inputs.tag_ci }} \ --target $DOCKERFILE_TARGET \ - --build-arg ARG_NODE_ENV=$DOCKERFILE_TARGET \ + --build-arg ARG_NODE_ENV=${{ github.event.inputs.cr_env }} \ --build-arg ARG_API_PORT=3000 \ $DOCKERFILE_PATH || exit 1001 - docker push $CR_HOST/$CR_NS/$CR_IMAGENAME:${{ github.event.inputs.tag_ci }} + docker push $CR_HOST/$CR_NS/$CR_IMAGENAME-${{ github.event.inputs.cr_env }}:${{ github.event.inputs.tag_ci }} docker logout diff --git a/app.js b/app.js index 411b9ad..50028ae 100644 --- a/app.js +++ b/app.js @@ -1,9 +1,9 @@ -"use strict"; +'use strict'; -const url = require("url"); -const http = require("http"); -const https = require("https"); -const express = require("express"); +const url = require('url'); +const http = require('http'); +const https = require('https'); +const express = require('express'); const app = express(); // using CJS in ESM sibling-module.js is a CommonJS module @@ -11,9 +11,9 @@ const app = express(); // const require = createRequire(import.meta.url) // const siblingModule = require('./sibling-module') -const { sleep } = require("esm")(module)("@es-labs/esm/sleep"); +const { sleep } = require('esm')(module)('@es-labs/esm/sleep'); -require("@es-labs/node/express/init")(); +require('@es-labs/node/express/init')(); // setup graceful exit const handleExitSignal = async (signal) => await cleanup(`Signal ${signal}`, 0); // NOSONAR @@ -24,11 +24,11 @@ const handleExitException = async (err, origin) => ); // NOSONAR const handleExitRejection = async (reason, promise) => await cleanup(`Unhandled Rejection. reason: ${reason?.stack || reason}`, 1); // NOSONAR -process.on("SIGINT", handleExitSignal); -process.on("SIGTERM", handleExitSignal); -process.on("SIGQUIT", handleExitSignal); -process.on("uncaughtException", handleExitException); -process.on("unhandledRejection", handleExitRejection); +process.on('SIGINT', handleExitSignal); +process.on('SIGTERM', handleExitSignal); +process.on('SIGQUIT', handleExitSignal); +process.on('uncaughtException', handleExitException); +process.on('unhandledRejection', handleExitRejection); const { HTTPS_PRIVATE_KEY, HTTPS_CERTIFICATE } = process.env; const https_opts = {}; @@ -46,10 +46,10 @@ const server = HTTPS_CERTIFICATE // USERLAND - Add APM tool -require("@es-labs/node/express/preRoute")(app, express); -const graphqlWsServer = require("@es-labs/node/express/graphql")(app, server); -const services = require("@es-labs/node/services"); -const authService = require("@es-labs/node/auth"); +require('@es-labs/node/express/preRoute')(app, express); +const graphqlWsServer = require('@es-labs/node/express/graphql')(app, server); +const services = require('@es-labs/node/services'); +const authService = require('@es-labs/node/auth'); // CLEANUP const cleanup = async ( @@ -75,9 +75,9 @@ const cleanup = async ( } }); } - console.log("cleaning up and awaiting exit..."); + console.log('cleaning up and awaiting exit...'); await sleep(timeOutMs); // from here on... does not get called on uncaught exception crash - console.log("exiting"); // require('fs').writeSync(process.stderr.fd, `bbbbbbbbbbbb`) + console.log('exiting'); // require('fs').writeSync(process.stderr.fd, `bbbbbbbbbbbb`) return coreDump ? process.abort : process.exit(exitCode); // setTimeout(() => console.log('exiting'), timeOutMs).unref() }; @@ -85,7 +85,7 @@ const cleanup = async ( // SERVICES services.start() try { - authService.setup(services.get("keyv"), services.get("knex1")); // setup authorization + authService.setup(services.get('keyv'), services.get('knex1')); // setup authorization } catch (e) { console.log(e) } @@ -134,13 +134,13 @@ Layer.prototype.handle_request = function(req, res, next) { } try { - require(`./apps/apploader`)(app); // add your APIs here - require("./router")(app); // common routes - app.use("/api/**", (req, res) => - res.status(404).json({ error: "Not Found" }) + require('./apps/apploader')(app); // add your APIs here + require('./router')(app); // common routes + app.use('/api/**', (req, res) => + res.status(404).json({ error: 'Not Found' }) ); } catch (e) { - console.log("Route loading exception", e.toString()) + console.log('Route loading exception', e.toString()) } // END ROUTES @@ -149,36 +149,36 @@ const { OPENAPI_OPTIONS } = process.env const openApiOptions = JSON.parse(OPENAPI_OPTIONS || null) if (openApiOptions) { openApiOptions.baseDir = __dirname - const expressJSDocSwagger = require("express-jsdoc-swagger"); + const expressJSDocSwagger = require('express-jsdoc-swagger'); expressJSDocSwagger(app)(openApiOptions); } // websockets -server.on("upgrade", (request, socket, head) => { +server.on('upgrade', (request, socket, head) => { const pathname = url.parse(request.url).pathname; - if (pathname === "/subscriptions") { + if (pathname === '/subscriptions') { // upgrade the graphql server graphqlWsServer.handleUpgrade(request, socket, head, (ws) => { - graphqlWsServer.emit("connection", ws, request); + graphqlWsServer.emit('connection', ws, request); }); } - console.log("upgrade event"); + console.log('upgrade event'); }); -require("@es-labs/node/express/postRoute")(app, express); +require('@es-labs/node/express/postRoute')(app, express); // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status // 'Bad Request': 400, 'Unauthorized': 401, 'Forbidden': 403, 'Not Found': 404, 'Conflict': 409, 'Unprocessable Entity': 422, 'Internal Server Error': 500, app.use((error, req, res, next) => { // error middleware - 200s should not reach here // console.log('typeof error', error instanceof Error) - console.log("error middleware", error); - let message = "Unknown Error"; + console.log('error middleware', error); + let message = 'Unknown Error'; if (error.message) { // console.log('Error Object', error.name, error.name, error.stack) message = - process.env.NODE_ENV === "development" ? error.stack : error.message; - } else if (typeof error === "string") { + process.env.NODE_ENV === 'development' ? error.stack : error.message; + } else if (typeof error === 'string') { message = error; } else if (error?.toString) { message = error.toString(); diff --git a/apps/.env.dev b/apps/.env.dev new file mode 100644 index 0000000..e39307b --- /dev/null +++ b/apps/.env.dev @@ -0,0 +1,383 @@ +STACK_TRACE_LIMIT=3 + +API_PORT=3000 + +WS_OPTIONS='{ + "WS_PORT": 3001, + "WS_KEEEPALIVE_MS": 30000 +}' + +# AUTH +SALT_ROUNDS=12 + +# HTTPONLY COOKIES +# https://web.dev/samesite-cookies-explained/ +# true = use HttpOnly cookie, false - do not use HttpOnly cookie (alternatively use localStorage / sessionStorage - be careful, has security implications) +COOKIE_HTTPONLY=1 # (also set on FE... credentials if cross origin, true means ) - DO TAKE NOTE OF CORS +# must be true if COOKIE_SAMESITE=None +COOKIE_SECURE= +# Strict - CORS_OPTIONS === null +# Lax, None (None must use Secure also) - CORS_OPTIONS !== null +COOKIE_SAMESITE=Lax +COOKIE_MAXAGE= +COOKIE_SECRET= # for use by cookie-parser +COOKIE_DOMAIN= + +# UAT +# JWT_EXPIRY=7200 +# JWT_REFRESH_EXPIRY=14400 +# COOKIE_SECURE= +# COOKIE_SAMESITE=None + +AUTH_REFRESH_URL=/api/auth/refresh +AUTH_USER_STORE=knex # mongo, knex +AUTH_USER_STORE_NAME=users +AUTH_USER_FIELD_ID_FOR_JWT=id # mongo = _id, knex = id // can be NTID from SAML +AUTH_USER_FIELDS_JWT_PAYLOAD=email,groups # comma seperated, can be AD Groups from SAML, email, etc. +AUTH_USER_FIELD_LOGIN=email +AUTH_USER_FIELD_PASSWORD=password +AUTH_USER_FIELD_GAKEY=gaKey + +# AUTH JWT - secret key +JWT_ALG=HS256 # 'RS256' (use SSL certs), 'HS256' (use secret string) +JWT_SECRET=123456789 # HS256 +JWT_REFRESH_SECRET=123456789 # HS256 +JWT_EXPIRY=45m # 5s (test refresh) // 1800 // '150d', '15d', '15m', '15s', use small expiry to test refresh mechanism, numeric is seconds +JWT_REFRESH_EXPIRY=1h # 10s (test refresh) // 3600 // do not allow refresh handling after defined seconds +JWT_REFRESH_STORE=keyv # mongo, knex, redis, keyv (default) +JWT_REFRESH_STORE_NAME=user_session # collection or table name +JWT_ALLOW_INSECURE_KEY_SIZES=1 # leave empty to disallow insecure key size + +# AUTH - OTP +USE_OTP=TEST # GA, SMS, '' (also on FE) set to TEST for testing using 111111 as PIN +OTP_EXPIRY=30 # in seconds - UNUSED + +# URL to redirect if error +AUTH_ERROR_URL= + +# SAML - SECRET +# https://github.com/node-saml/passport-saml#config-parameter-details +SAML_OPTIONS='{ + "cert": "", + "callbackUrl": "http://127.0.0.1:3000/api/saml/callback", + "entryPoint": "http://127.0.0.1:8081/realms/dev/protocol/saml", + "identifierFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", + "issuer": "dev-client-saml", + "wantAuthnResponseSigned": true, + "wantAssertionsSigned": false, + "signatureAlgorithm": "sha256" +}' + +SAML_CERTIFICATE= +SAML_PRIVATE_KEY= +DISABLED_SAML_PRIVATE_KEY='{ "privateKey": "", "decryptionPvk": "" }' + +SAML_JWT_MAP='{ "id": "nameID", "groups": "Role" }' + +# OIDC - SECRET +# NOTE: need to setup keycloak for OIDC see readme in docker-devenv folder +## For SPA - http://127.0.0.1:8080/callback +## For express hosted sample frontend - http://127.0.0.1:3000/sso.html +# OIDC_OPTIONS= +# - REISSUE = tokens issued by app instead of IDP, refresh endpoint also handled by app +# - ID_NAME = the property name for user id in oidc token from IDP +OIDC_OPTIONS='{ + "URL": "http://127.0.0.1:8081/realms/dev/protocol/openid-connect", + "CLIENT_ID": "dev-client-oidc", + "CLIENT_SECRET": "", + "CALLBACK": "http://127.0.0.1:3000/sso.html", + "REISSUE": true, + "ID_NAME": "preferred_username" +}' + +# OAuth - SECRET (we use github for example) +## For SPA - http://127.0.0.1:8080/callback +## For express hosted sample frontend - http://127.0.0.1:3000/sso.html +OAUTH_OPTIONS='{ + "URL": "https://github.com/login/oauth/access_token", + "CLIENT_ID": "a355948a635c2a2066e2", + "CLIENT_SECRET": "", + "CALLBACK": "http://127.0.0.1:3000/sso.html", + "USER_URL": "https://api.github.com/user", + "USER_ID": "id", + "USER_GROUPS": "", + "FIND_ID": "githubId" +}' + +# MONGO DB INFO +# - SECRET, SHOULD STORE IN SEPERATE AES ENCRYPTED FILE IN PROD +# url=mongodb://{USER}:{PASSWORD}@{HOST}:{PORT}/{DBNAME}?authMechanism=SCRAM-SHA-1&authSource={AUTH_DBNAME} +# url=mongodb://127.0.0.1:27017/mm?replicaSet=rs0 +# https://mongodb.github.io/node-mongodb-native/3.6/reference/connecting/connection-settings/ +# https://github.com/Automattic/mongoose/issues/8180 +# small value timedout on cloudrun +MONGO_OPTIONS= +# MONGO_OPTIONS='{ +# "url": "mongodb://127.0.0.1:27017/testdb-development", +# "opts": { +# "useUnifiedTopology": true, +# "useNewUrlParser": true, +# "connectTimeoutMS": 30000, +# "serverSelectionTimeoutMS": 30000 +# } +# }' + +# knexfile configuration - SECRET +# sqilite conection.filename - relative to directory that package.json was run +# KNEXFILE= +KNEXFILE='{ + "client": "sqlite3", + "connection": { "filename": "apps/app-sample/dev.sqlite3" }, + "useNullAsDefault": true +}' + +# GCP Stuff - SECRET +# GCP SERVICE KEY is { } +GCP_SERVICE_KEY= +DISABLED_GCP_SERVICE_KEY='{ + "type": "service_account", + "project_id": "", + "private_key_id": "", + "private_key": "", + "client_email": "", + "client_id": "", + "auth_uri": "", + "token_uri": "", + "auth_provider_x509_cert_url": "", + "client_x509_cert_url": "" +}' +GCP_DEFAULT_BUCKET= + + +# types: redis=REDIS_CONFIG, mongo=MONGO_OPTIONS +SERVICES_TYPES_AVAILABLE=auth,ws,knex,keyv +SERVICES_CONFIG='[ + { + "name": "knex1", + "type": "knex", + "options": "KNEXFILE" + }, + { + "name": "keyv", + "type": "keyv", + "options": "KEYV_CACHE" + }, + { + "name": "ws", + "type": "ws", + "options": "WS_OPTIONS" + } +]' + + + +# helmetjs options +HELMET_OPTIONS= +# if you enable below the pages served by this server may not work +DISABLED_HELMET_OPTIONS='{ + "hideServer": true, + "csp": null, + "nosniff": true, + "xssfilter": true, + "csp": { + "directives": { + "defaultSrc": ["self"], + "scriptSrc": ["self","code.jquery.com","maxcdn.bootstrapcdn.com"], + "styleSrc": ["self","maxcdn.bootstrapcdn.com"], + "fontSrc": ["self","maxcdn.bootstrapcdn.com"] + } + } +}' + +# CORS - SAME ORIGIN - PROXIED +# CORS_OPTIONS: null +# PROXY_WWW_ORIGIN: 'example.com:8080' +# WEB_STATIC: '' + +# CORS - SAME ORIGIN - SERVED BY EXPRESS STATIC +# CORS_OPTIONS: null +# PROXY_WWW_ORIGIN: '' +# WEB_STATIC: 'public' + +# CORS - CROSS ORIGIN +# CORS_OPTIONS { +# ... +# withCredentials: true, +# origin: '127.0.0.1:8080' +# } +# PROXY_WWW_ORIGIN: '' +# WEB_STATIC: '' + + +# port 8081 is from SAML +# nip.io & localhost for fido2 testing +# DEV - TODELETE CORS_ORIGINS? +CORS_ORIGINS= +# CORS_ORIGINS="http://127.0.0.1:8080,http://127.0.0.1:8081,https://127.0.0.1:8080,https://127.0.0.1:8081,https://192-168-18-8.nip.io:3000,http://localhost:3000" +# UAT +# CORS_ORIGINS="https://uat.mybot.live,http://uat.mybot.live" + +# if withCredentials === false at Frontend +# CORS_OPTIONS= +# +# set withCredentials === true at Frontend + # exposedHeaders: ['refresh-token'], // allow this to be sent back in response + # maxAge + # allowedHeaders + # credentials + # default cors settings + # credentials: true // Access-Control-Allow-Credentials value to true +CORS_OPTIONS='{ + "methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "preflightContinue": false, + "optionsSuccessStatus": 204, + "credentials": true, + "origin": ["http://127.0.0.1:8080","https://127.0.0.1:8080"] +}' + +# "allowedHeaders": ["content-type", "Authorization","access_token", "refresh_token"] + +# 'http://127.0.0.1:8080', // used by proxy middleware +PROXY_WWW_ORIGIN= + +# serve static content - folder is relative to calling package.json - https://expressjs.com/en/4x/api.html#express.static +# serve website from folder, blank if do not serve from express. Must be '' if there is PROXY_WWW_ORIGIN +# options does not seem to work +# { folder: 'openapi', url: '/ftp2', options: {'icons': true}, list: true }, +# WEB_STATIC= # API only..., not serve any static page +WEB_STATIC='[ + { "folder": "node_modules/@es-labs/esm", "url": "/esm" }, + { "folder": "apps/app-sample/public/vue-nobundler", "url": "/native", "options": { "extensions": ["html"], "index": false } }, + { "folder": "apps/app-sample/public/demo-express", "url": "/" } +]' +# last as path is / + +## TBD +# for file uploads +UPLOAD_STATIC='[{ + "folder": "apps/app-sample/uploads", + "url": "/uploads", + "options": { + "fileFilter": { + "allowedMimeTypes": "text,image", + "allowedExtensions": "" + }, + "limits": { + "files": 2, + "fileSize": 10000000 + } + }, + "list": true, + "listOptions": { + "icons": true + } +}]' + +## TBD +# size in bytes +# fileFilter property... +UPLOAD_MEMORY='[{ + "limits": { + "files" : 1, + "fileSize": 500000 + } +}]' + +# Role-based access control - not needed, implemented by middleware - e.g. isAdmin after user authentication + +# master list of config keys - defaults will be undefined unless specified + +# Express - logging - refer to common-express/preRoute.js +ENABLE_LOGGER= + +# MQ - TBD + +# Communications - Nexmo - @es-labs/node/comms/nexmo.js +NEXMO_SENDER= +NEXMO_KEY= +NEXMO_SECRET='' + +# Communications - Telegram - @es-labs/node/comms/telegram.js +TELEGRAM_CHANNEL_ID= +TELEGRAM_API_KEY='' + +# Communications - Sendgrid - @es-labs/node/comms/email.js +SENDGRID_KEY= +SENDGRID_SENDER='' + +# Communications - Firebase Messaging - @es-labs/node/comms/fcm.js +FCM_SERVER_KEY='' + +# Communications - Firebase Messaging (@es-labs/node/comms/webpush.js) - http://127.0.0.1:3000, https://localhost:3000 +WEBPUSH_VAPID_SUBJ=https://127.0.0.1:3000 + +# Caching Redis - ioredis object or null +# if using sentinels +# opts.sentinels: [{ "host": "localhost", "port": 26379 }, { "host": "localhost", "port": 26380 }] +# opts.name: "mymaster", +REDIS_CONFIG= +# REDIS_CONFIG='{ +# "opts": { +# "port": 6379, +# "host": "127.0.0.1", +# "family": 4, +# "password": '', +# "db": 0, +# "maxRetriesPerRequest": 20, +# "autoResubscribe": true, // default +# "autoResendUnfulfilledCommands": true, +# }, +# "retry": { +# "step": 50, "max": 2000 +# }, +# "reconnect": { +# "targetError": "READONLY" +# } +# }' + +# Caching Keyv - keyv object or null +KEYV_CACHE='{}' + +# bodyparser +BODYPARSER_JSON='{ "limit": "2mb" }' +BODYPARSER_URLENCODED='{ "extended": true, "limit": "2mb" }' + +# OPENAPI document - express-jsdoc-swagger +OPENAPI_OPTIONS='{ + "info": { + "version": "0.0.1", + "title": "Express Template", + "description": "Please log in an get token (could be http-only) from http://127.0.0.1:3000/refresh-token.html, also logout there (for http-only tokens)", + "license": { + "name": "MIT" + } + }, + "security": { + "BearerAuth": { + "type": "http", + "scheme": "bearer" + } + }, + "filesPattern": ["./**/*.js"] +}' + +# GraphQL - WAIT FOR THIS TO BE FIXED - https://github.com/graphql/express-graphql/pull/793 +# npm i graphql-subscriptions, graphql, express-graphql, graphql-ws +# "express-graphql": "^0.12.0", +# "graphql": "^16.3.0", +# "graphql-subscriptions": "^2.0.0", +# "graphql-ws": "^5.6.2", +# GRAPHQL_SCHEMA_PATH=../apps/graphql-schema +GRAPHQL_SCHEMA_PATH= +GRAPHQL_URL=/graphql +GRAPHQL_SUB_URL=/subscriptions + +JWT_PRIVATE_KEY= +JWT_CERTIFICATE= + +JWT_REFRESH_PRIVATE_KEY= +JWT_REFRESH_CERTIFICATE= + +HTTPS_PRIVATE_KEY= +HTTPS_CERTIFICATE= + diff --git a/apps/.env.development b/apps/.env.development new file mode 100644 index 0000000..e39307b --- /dev/null +++ b/apps/.env.development @@ -0,0 +1,383 @@ +STACK_TRACE_LIMIT=3 + +API_PORT=3000 + +WS_OPTIONS='{ + "WS_PORT": 3001, + "WS_KEEEPALIVE_MS": 30000 +}' + +# AUTH +SALT_ROUNDS=12 + +# HTTPONLY COOKIES +# https://web.dev/samesite-cookies-explained/ +# true = use HttpOnly cookie, false - do not use HttpOnly cookie (alternatively use localStorage / sessionStorage - be careful, has security implications) +COOKIE_HTTPONLY=1 # (also set on FE... credentials if cross origin, true means ) - DO TAKE NOTE OF CORS +# must be true if COOKIE_SAMESITE=None +COOKIE_SECURE= +# Strict - CORS_OPTIONS === null +# Lax, None (None must use Secure also) - CORS_OPTIONS !== null +COOKIE_SAMESITE=Lax +COOKIE_MAXAGE= +COOKIE_SECRET= # for use by cookie-parser +COOKIE_DOMAIN= + +# UAT +# JWT_EXPIRY=7200 +# JWT_REFRESH_EXPIRY=14400 +# COOKIE_SECURE= +# COOKIE_SAMESITE=None + +AUTH_REFRESH_URL=/api/auth/refresh +AUTH_USER_STORE=knex # mongo, knex +AUTH_USER_STORE_NAME=users +AUTH_USER_FIELD_ID_FOR_JWT=id # mongo = _id, knex = id // can be NTID from SAML +AUTH_USER_FIELDS_JWT_PAYLOAD=email,groups # comma seperated, can be AD Groups from SAML, email, etc. +AUTH_USER_FIELD_LOGIN=email +AUTH_USER_FIELD_PASSWORD=password +AUTH_USER_FIELD_GAKEY=gaKey + +# AUTH JWT - secret key +JWT_ALG=HS256 # 'RS256' (use SSL certs), 'HS256' (use secret string) +JWT_SECRET=123456789 # HS256 +JWT_REFRESH_SECRET=123456789 # HS256 +JWT_EXPIRY=45m # 5s (test refresh) // 1800 // '150d', '15d', '15m', '15s', use small expiry to test refresh mechanism, numeric is seconds +JWT_REFRESH_EXPIRY=1h # 10s (test refresh) // 3600 // do not allow refresh handling after defined seconds +JWT_REFRESH_STORE=keyv # mongo, knex, redis, keyv (default) +JWT_REFRESH_STORE_NAME=user_session # collection or table name +JWT_ALLOW_INSECURE_KEY_SIZES=1 # leave empty to disallow insecure key size + +# AUTH - OTP +USE_OTP=TEST # GA, SMS, '' (also on FE) set to TEST for testing using 111111 as PIN +OTP_EXPIRY=30 # in seconds - UNUSED + +# URL to redirect if error +AUTH_ERROR_URL= + +# SAML - SECRET +# https://github.com/node-saml/passport-saml#config-parameter-details +SAML_OPTIONS='{ + "cert": "", + "callbackUrl": "http://127.0.0.1:3000/api/saml/callback", + "entryPoint": "http://127.0.0.1:8081/realms/dev/protocol/saml", + "identifierFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", + "issuer": "dev-client-saml", + "wantAuthnResponseSigned": true, + "wantAssertionsSigned": false, + "signatureAlgorithm": "sha256" +}' + +SAML_CERTIFICATE= +SAML_PRIVATE_KEY= +DISABLED_SAML_PRIVATE_KEY='{ "privateKey": "", "decryptionPvk": "" }' + +SAML_JWT_MAP='{ "id": "nameID", "groups": "Role" }' + +# OIDC - SECRET +# NOTE: need to setup keycloak for OIDC see readme in docker-devenv folder +## For SPA - http://127.0.0.1:8080/callback +## For express hosted sample frontend - http://127.0.0.1:3000/sso.html +# OIDC_OPTIONS= +# - REISSUE = tokens issued by app instead of IDP, refresh endpoint also handled by app +# - ID_NAME = the property name for user id in oidc token from IDP +OIDC_OPTIONS='{ + "URL": "http://127.0.0.1:8081/realms/dev/protocol/openid-connect", + "CLIENT_ID": "dev-client-oidc", + "CLIENT_SECRET": "", + "CALLBACK": "http://127.0.0.1:3000/sso.html", + "REISSUE": true, + "ID_NAME": "preferred_username" +}' + +# OAuth - SECRET (we use github for example) +## For SPA - http://127.0.0.1:8080/callback +## For express hosted sample frontend - http://127.0.0.1:3000/sso.html +OAUTH_OPTIONS='{ + "URL": "https://github.com/login/oauth/access_token", + "CLIENT_ID": "a355948a635c2a2066e2", + "CLIENT_SECRET": "", + "CALLBACK": "http://127.0.0.1:3000/sso.html", + "USER_URL": "https://api.github.com/user", + "USER_ID": "id", + "USER_GROUPS": "", + "FIND_ID": "githubId" +}' + +# MONGO DB INFO +# - SECRET, SHOULD STORE IN SEPERATE AES ENCRYPTED FILE IN PROD +# url=mongodb://{USER}:{PASSWORD}@{HOST}:{PORT}/{DBNAME}?authMechanism=SCRAM-SHA-1&authSource={AUTH_DBNAME} +# url=mongodb://127.0.0.1:27017/mm?replicaSet=rs0 +# https://mongodb.github.io/node-mongodb-native/3.6/reference/connecting/connection-settings/ +# https://github.com/Automattic/mongoose/issues/8180 +# small value timedout on cloudrun +MONGO_OPTIONS= +# MONGO_OPTIONS='{ +# "url": "mongodb://127.0.0.1:27017/testdb-development", +# "opts": { +# "useUnifiedTopology": true, +# "useNewUrlParser": true, +# "connectTimeoutMS": 30000, +# "serverSelectionTimeoutMS": 30000 +# } +# }' + +# knexfile configuration - SECRET +# sqilite conection.filename - relative to directory that package.json was run +# KNEXFILE= +KNEXFILE='{ + "client": "sqlite3", + "connection": { "filename": "apps/app-sample/dev.sqlite3" }, + "useNullAsDefault": true +}' + +# GCP Stuff - SECRET +# GCP SERVICE KEY is { } +GCP_SERVICE_KEY= +DISABLED_GCP_SERVICE_KEY='{ + "type": "service_account", + "project_id": "", + "private_key_id": "", + "private_key": "", + "client_email": "", + "client_id": "", + "auth_uri": "", + "token_uri": "", + "auth_provider_x509_cert_url": "", + "client_x509_cert_url": "" +}' +GCP_DEFAULT_BUCKET= + + +# types: redis=REDIS_CONFIG, mongo=MONGO_OPTIONS +SERVICES_TYPES_AVAILABLE=auth,ws,knex,keyv +SERVICES_CONFIG='[ + { + "name": "knex1", + "type": "knex", + "options": "KNEXFILE" + }, + { + "name": "keyv", + "type": "keyv", + "options": "KEYV_CACHE" + }, + { + "name": "ws", + "type": "ws", + "options": "WS_OPTIONS" + } +]' + + + +# helmetjs options +HELMET_OPTIONS= +# if you enable below the pages served by this server may not work +DISABLED_HELMET_OPTIONS='{ + "hideServer": true, + "csp": null, + "nosniff": true, + "xssfilter": true, + "csp": { + "directives": { + "defaultSrc": ["self"], + "scriptSrc": ["self","code.jquery.com","maxcdn.bootstrapcdn.com"], + "styleSrc": ["self","maxcdn.bootstrapcdn.com"], + "fontSrc": ["self","maxcdn.bootstrapcdn.com"] + } + } +}' + +# CORS - SAME ORIGIN - PROXIED +# CORS_OPTIONS: null +# PROXY_WWW_ORIGIN: 'example.com:8080' +# WEB_STATIC: '' + +# CORS - SAME ORIGIN - SERVED BY EXPRESS STATIC +# CORS_OPTIONS: null +# PROXY_WWW_ORIGIN: '' +# WEB_STATIC: 'public' + +# CORS - CROSS ORIGIN +# CORS_OPTIONS { +# ... +# withCredentials: true, +# origin: '127.0.0.1:8080' +# } +# PROXY_WWW_ORIGIN: '' +# WEB_STATIC: '' + + +# port 8081 is from SAML +# nip.io & localhost for fido2 testing +# DEV - TODELETE CORS_ORIGINS? +CORS_ORIGINS= +# CORS_ORIGINS="http://127.0.0.1:8080,http://127.0.0.1:8081,https://127.0.0.1:8080,https://127.0.0.1:8081,https://192-168-18-8.nip.io:3000,http://localhost:3000" +# UAT +# CORS_ORIGINS="https://uat.mybot.live,http://uat.mybot.live" + +# if withCredentials === false at Frontend +# CORS_OPTIONS= +# +# set withCredentials === true at Frontend + # exposedHeaders: ['refresh-token'], // allow this to be sent back in response + # maxAge + # allowedHeaders + # credentials + # default cors settings + # credentials: true // Access-Control-Allow-Credentials value to true +CORS_OPTIONS='{ + "methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "preflightContinue": false, + "optionsSuccessStatus": 204, + "credentials": true, + "origin": ["http://127.0.0.1:8080","https://127.0.0.1:8080"] +}' + +# "allowedHeaders": ["content-type", "Authorization","access_token", "refresh_token"] + +# 'http://127.0.0.1:8080', // used by proxy middleware +PROXY_WWW_ORIGIN= + +# serve static content - folder is relative to calling package.json - https://expressjs.com/en/4x/api.html#express.static +# serve website from folder, blank if do not serve from express. Must be '' if there is PROXY_WWW_ORIGIN +# options does not seem to work +# { folder: 'openapi', url: '/ftp2', options: {'icons': true}, list: true }, +# WEB_STATIC= # API only..., not serve any static page +WEB_STATIC='[ + { "folder": "node_modules/@es-labs/esm", "url": "/esm" }, + { "folder": "apps/app-sample/public/vue-nobundler", "url": "/native", "options": { "extensions": ["html"], "index": false } }, + { "folder": "apps/app-sample/public/demo-express", "url": "/" } +]' +# last as path is / + +## TBD +# for file uploads +UPLOAD_STATIC='[{ + "folder": "apps/app-sample/uploads", + "url": "/uploads", + "options": { + "fileFilter": { + "allowedMimeTypes": "text,image", + "allowedExtensions": "" + }, + "limits": { + "files": 2, + "fileSize": 10000000 + } + }, + "list": true, + "listOptions": { + "icons": true + } +}]' + +## TBD +# size in bytes +# fileFilter property... +UPLOAD_MEMORY='[{ + "limits": { + "files" : 1, + "fileSize": 500000 + } +}]' + +# Role-based access control - not needed, implemented by middleware - e.g. isAdmin after user authentication + +# master list of config keys - defaults will be undefined unless specified + +# Express - logging - refer to common-express/preRoute.js +ENABLE_LOGGER= + +# MQ - TBD + +# Communications - Nexmo - @es-labs/node/comms/nexmo.js +NEXMO_SENDER= +NEXMO_KEY= +NEXMO_SECRET='' + +# Communications - Telegram - @es-labs/node/comms/telegram.js +TELEGRAM_CHANNEL_ID= +TELEGRAM_API_KEY='' + +# Communications - Sendgrid - @es-labs/node/comms/email.js +SENDGRID_KEY= +SENDGRID_SENDER='' + +# Communications - Firebase Messaging - @es-labs/node/comms/fcm.js +FCM_SERVER_KEY='' + +# Communications - Firebase Messaging (@es-labs/node/comms/webpush.js) - http://127.0.0.1:3000, https://localhost:3000 +WEBPUSH_VAPID_SUBJ=https://127.0.0.1:3000 + +# Caching Redis - ioredis object or null +# if using sentinels +# opts.sentinels: [{ "host": "localhost", "port": 26379 }, { "host": "localhost", "port": 26380 }] +# opts.name: "mymaster", +REDIS_CONFIG= +# REDIS_CONFIG='{ +# "opts": { +# "port": 6379, +# "host": "127.0.0.1", +# "family": 4, +# "password": '', +# "db": 0, +# "maxRetriesPerRequest": 20, +# "autoResubscribe": true, // default +# "autoResendUnfulfilledCommands": true, +# }, +# "retry": { +# "step": 50, "max": 2000 +# }, +# "reconnect": { +# "targetError": "READONLY" +# } +# }' + +# Caching Keyv - keyv object or null +KEYV_CACHE='{}' + +# bodyparser +BODYPARSER_JSON='{ "limit": "2mb" }' +BODYPARSER_URLENCODED='{ "extended": true, "limit": "2mb" }' + +# OPENAPI document - express-jsdoc-swagger +OPENAPI_OPTIONS='{ + "info": { + "version": "0.0.1", + "title": "Express Template", + "description": "Please log in an get token (could be http-only) from http://127.0.0.1:3000/refresh-token.html, also logout there (for http-only tokens)", + "license": { + "name": "MIT" + } + }, + "security": { + "BearerAuth": { + "type": "http", + "scheme": "bearer" + } + }, + "filesPattern": ["./**/*.js"] +}' + +# GraphQL - WAIT FOR THIS TO BE FIXED - https://github.com/graphql/express-graphql/pull/793 +# npm i graphql-subscriptions, graphql, express-graphql, graphql-ws +# "express-graphql": "^0.12.0", +# "graphql": "^16.3.0", +# "graphql-subscriptions": "^2.0.0", +# "graphql-ws": "^5.6.2", +# GRAPHQL_SCHEMA_PATH=../apps/graphql-schema +GRAPHQL_SCHEMA_PATH= +GRAPHQL_URL=/graphql +GRAPHQL_SUB_URL=/subscriptions + +JWT_PRIVATE_KEY= +JWT_CERTIFICATE= + +JWT_REFRESH_PRIVATE_KEY= +JWT_REFRESH_CERTIFICATE= + +HTTPS_PRIVATE_KEY= +HTTPS_CERTIFICATE= + diff --git a/apps/.gitignore b/apps/.gitignore index 0bb6dc7..c573993 100644 --- a/apps/.gitignore +++ b/apps/.gitignore @@ -6,7 +6,18 @@ !package.json # EDIT BELOW - comment out environments you want in your project -# !.env.development + +# local machine +!.env.development +# !.env.secret.development + +# server deployment +!.env.dev +# !.env.secret.dev +# !.env.uat +# !.env.secret.uat +# !.env.prd +# !.env.secret.prd # EDIT BELOW - comment out folders you want in your project !app-auth diff --git a/apps/README.md b/apps/README.md index e4fc0b5..89d9a86 100644 --- a/apps/README.md +++ b/apps/README.md @@ -4,8 +4,8 @@ ## Configuration (Environment Files) -- .env : non-sensitive config values -- .env.secret : values that are secret (should be in `vault` service for production) +- .env. : non-sensitive config values +- .env.secret. : values that are secret (should be in `vault` service for production) - JSON values are supported, be aware of syntax errors when setting up ## Some Features diff --git a/apps/app-sample/test.http b/apps/app-sample/test.http index e41f578..a69ead6 100644 --- a/apps/app-sample/test.http +++ b/apps/app-sample/test.http @@ -1,8 +1,13 @@ # Using - https://github.com/Huachao/vscode-restclient +### Test healthcheck +GET http://127.0.0.1:3000/api/healthcheck +# GET http://127.0.0.1:3000/api/app-sample/healthcheck +# GET http://127.0.0.1:3000/api/app-sample/check-db + ### HTTP Testing -https://randomuser.me/api/ +GET https://randomuser.me/api/ Accept: application/json ### Test Post @@ -10,13 +15,6 @@ Accept: application/json POST https://httpbin.org/post Accept: application/json -### Test healthcheck -# GET http://127.0.0.1:3000/api/healthcheck -# GET http://pay.t2tkn.com:3100/api/healthcheck -# GET http://127.0.0.1:3000/api/app-sample/healthcheck -GET http://127.0.0.1:3000/api/app-sample/check-db - - ### Test Login - Username & Password POST http://127.0.0.1:3000/api/auth/login content-type: application/json diff --git a/deploy.sh b/deploy.sh index b7d9db2..40efead 100644 --- a/deploy.sh +++ b/deploy.sh @@ -1,4 +1,5 @@ #!/bin/bash +# TBD # $0. The name of script itself. # $$ Process id of current shell. # $* Values of all the arguments. ... diff --git a/env.js b/env.js index 2f79c20..58d6f99 100644 --- a/env.js +++ b/env.js @@ -1,5 +1,7 @@ const path = require('path') const dotenv = require('dotenv') -dotenv.config({ path: path.join(process.cwd(), 'apps', '.env'), override: true } ) -dotenv.config({ path: path.join(process.cwd(), 'apps', '.env.secret'), override: true } ) +const { NODE_ENV } = process.env + +dotenv.config({ path: path.join(process.cwd(), 'apps', `.env.${NODE_ENV}`), override: true } ) +dotenv.config({ path: path.join(process.cwd(), 'apps', `.env.secret.${NODE_ENV}`), override: true } ) diff --git a/index.js b/index.js index b9546ee..e02b9b5 100644 --- a/index.js +++ b/index.js @@ -7,7 +7,7 @@ console.info('Globals setup and config done. Starting app... ') // if development && hostname == localhost allow TLS - call after config load - if (process.env.NODE_ENV === 'development') process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0 + if (process.env.NODE_ENV === 'development') process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0 const { server } = require('./app') const { API_PORT, HTTPS_CERTIFICATE } = process.env