diff --git a/api/environment-variables.md b/api/environment-variables.md index 022d57f66..8a8b76a57 100644 --- a/api/environment-variables.md +++ b/api/environment-variables.md @@ -1,52 +1,52 @@ # TruBudget-API -## Environment Variables +## Environment variables -| Env Variable | Required | Default Value | Description | -| ------------------------------------- | ---------------------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| LOG_LEVEL | no | info | Defines the log output. Supported levels are `trace`, `debug`, `info`, `warn`, `error`, `fatal` | -| ORGANIZATION | yes | - | In the blockchain network, each node is represented by its organization name. This environment variable sets this organization name. It is used to create the organization stream on the blockchain and is also displayed in the frontend's top right corner. | -| ORGANIZATION_VAULT_SECRET | yes | - | This is the key to en-/decrypt user data of an organization. If you want to add a new node for your organization, you want users to be able to log in on either node.
**Caution:** If you want to run TruBudget in production, make sure NOT to use the default value from the `.env.example` file! | -| PORT | no | 8080 | The port used to expose the API for your installation.
Example: If you run TruBudget locally and set API_PORT to `8080`, you can reach the API via `localhost:8080/api`. | -| PRETTY_PRINT | no | false | Decides whether the logs printed by the API are pretty printed or not. Pretty printed logs are easier to read while non-pretty printed logs are easier to store and use e.g. in the ELK (Elasticsearch-Logstash-Kabana) stack. | -| ROOT_SECRET | no | [random] | The root secret is the password for the root user. If you start with an empty blockchain, the root user is needed to add other users, approve new nodes,.. If you don't set a value via the environment variable, the API generates one randomly and prints it to the console
**Caution:** If you want to run TruBudget in production, make sure to set a secure root secret. | -| MULTICHAIN_RPC_HOST | no | localhost | The IP address of the blockchain (not multichain daemon,but they are usally the same) you want to connect to. | -| BLOCKCHAIN_PORT | no | 8085 | The Port of the blockchain where the server is available for incoming http connections (e.g. readiness, versions, backup and restore) | -| BLOCKCHAIN_PROTOCOL | no | http | The Protocol of the blockchain where the server is available for incoming http connections ("http" or "https") | -| MULTICHAIN_RPC_USER | no | multichainrpc | The user used to connect to the multichain daemon. | -| MULTICHAIN_RPC_PASSWORD | no | [hardcoded] | Password used by the API to connect to the blockchain. The password is set by the origin node upon start. Every beta node needs to use the same RPC password in order to be able to connect to the blockchain.
**Hint:** Although the MULTICHAIN_RPC_PASSWORD is not required it is highly recommended to set an own secure one | -| MULTICHAIN_RPC_PORT | no | 8000 | The port used to expose the multichain daemon of your Trubudget blockchain installation(bc). The port used to connect to the multichain daemon(api). This will be used internally for the communication between the API and the multichain daemon. | -| MULTICHAIN_RPC_PROTOCOL | no | http | The protocol used to expose the multichain daemon of your Trubudget blockchain installation(bc). The protocol used to connect to the multichain daemon(api). This will be used internally for the communication between the API and the multichain daemon. "http" or "https" | -| SWAGGER_BASEPATH `deprecated` | no | / | This variable was used to choose which environment (prod or test) is used for testing the requests. The variable is deprecated now, as the Swagger documentation can be used for the prod and test environment separately. | -| JWT_ALGORITHM | no | `HS256` | Algorithm used for signing and verifying JWTs. Currently `HS256` or `RS256` are supported. | -| JWT_SECRET | no | [random] | A string that is used to sign JWT which are created by the authenticate endpoint of the api. If JWT_ALGORITHM is set to `RS256`, this is required and holds BASE64 encoded PEM encoded private key for RSA. | -| JWT_PUBLIC_KEY | no | | If JWT_ALGORITHM is set to `RS256`, this is required and holds BASE64 encoded PEM encoded public key for RSA. | -| DOCUMENT_FEATURE_ENABLED | no | false | If true, all uploaded documents are stored using trubudget's storage-service. If false, the document feature of TruBudget is disabled, and trying to upload a document will result in an error. | -| DOCUMENT_EXTERNAL_LINKS_ENABLED | no | false | If true, it is possible to use external documents links also without TruBudget's storage-service. If false, the external documents links feature of TruBudget is still possible to use in case DOCUMENT_FEATURE_ENABLED equals "true". | -| STORAGE_SERVICE_HOST | no | localhost | IP of connected storage service | -| STORAGE_SERVICE_PORT | no | 8090 | Port of connected storage service | -| STORAGE_SERVICE_PROTOCOL | no | http | Protocol of connected storage service. "http" or "https" | -| STORAGE_SERVICE_EXTERNAL_URL | no | - | IP and port of own connected storage service accessible externally | -| EMAIL_HOST | no | localhost | IP of connected email service | -| EMAIL_PORT | no | 8090 | Port of connected email service | -| EMAIL_PROTOCOL | no | http | Protocol of connected email service. "http" or "https" | -| ENCRYPTION_PASSWORD | no | - | If set, all data that is send to the MultiChain node and external storage will be symmetrically encrypted by the ENCRYPTION_PASSWORD | -| SIGNING_METHOD | no | node | Possible signing methods are: `node` and `user`. Transactions on the chain will be signed using either the address of the node or the address of the specific user publishing that transaction. | -| NODE_ENV | no | production | If set to `development` api will allow any string as password. If set to `production` passwords must satisfy safePasswordSchema, see lib/joiValidation-.ts & -.spec.ts files | -| ACCESS_CONTROL_ALLOW_ORIGIN | no | "\*" | Since the service uses CORS, the domain by which it can be called needs to be set. Setting this value to `"*"` means that it can be called from any domain. Read more about this topic [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). | -| RATE_LIMIT | no | - | If set, API will limit the number of requests from any individual IP address to the set number per minute. Can be set to any `number`, but shouldn't be set too low. | -| AUTHPROXY_ENABLED | no | false | Enables logging in using the authorization token from authentication proxy | -| AUTHPROXY_JWS_SIGNATURE | no / yes if AUTHPROXY_ENABLED=true | - | secret/public key/certificate for verifying auth proxy token signature | -| APPLICATIONINSIGHTS_CONNECTION_STRING | no | - | Azure Application Insights Connection String | -| SILENCE_LOGGING_ON_FREQUENT_ROUTES | no | - | Set to "true" if you want to hide route logging on frequent and technical endpoints like `/readiness`, `/version`, etc. | -| REFRESH_TOKEN_STORAGE | no | - | Determining the type of storage for refresh tokens. Allowed values are "db" or "memory" | -| API_DB_USER | yes (if REFRESH_TOKEN_STORAGE=db) | - | Database user for database connection, e.g. postgres | -| API_DB_PASSWORD | yes (if REFRESH_TOKEN_STORAGE=db) | - | Database password for database connection | -| API_DB_TYPE | yes (if REFRESH_TOKEN_STORAGE=db) | - | Database type. Allowed values: "pg" | -| API_DB_HOST | yes (if REFRESH_TOKEN_STORAGE=db) | - | Database host | -| API_DB_DATABASE | yes (if REFRESH_TOKEN_STORAGE=db) | - | Name of the used database | -| API_DB_PORT | yes (if REFRESH_TOKEN_STORAGE=db) | - | Database port, e.g. 5432 | -| API_DB_SSL | yes (if REFRESH_TOKEN_STORAGE=db) | - | Database SSL connection. Allowed values: "true" or "false". | -| API_DB_SCHEMA | yes (if REFRESH_TOKEN_STORAGE=db) | - | Database schema, e.g. "public" | -| API_REFRESH_TOKENS_TABLE | yes (if REFRESH_TOKEN_STORAGE=db) | - | Name of table where refresh tokens will be stored, e.g. "refresh_token" | -| API_SECURE_COOKIE | no | true if NODE_ENV=production, otherwise false | | +| Env Variable name | Required | Default Value | Description | +| ----------------------------------------- | -------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **LOG_LEVEL** | no | info | Defines the log output. Allowed values: trace, debug, info, warn, error, fatal. | +| **ORGANIZATION** | yes | - | In the blockchain network, each node is represented by its organization name. This environment variable sets this organization name. It is used to create the organization stream on the blockchain and is also displayed in the frontend's top right corner. Minimal value: 1. Maximal value: 100. | +| **PORT** | no | 8091 | The port used to expose the API for your installation. Example: If you run TruBudget locally and set API_PORT to `8080`, you can reach the API via `localhost:8080/api`. Value is a port with minimal value 0 and maximal value 65535 | +| **ORGANIZATION_VAULT_SECRET** | yes | - | This is the key to en-/decrypt user data of an organization. If you want to add a new node for your organization, you want users to be able to log in on either node. **Caution:** If you want to run TruBudget in production, make sure NOT to use the default value from the `.env_example` file! Invalid values: secret. | +| **ROOT_SECRET** | yes | 4a8cdf9b1539144f641fb1e8843be1d6a3b4a109d0dac94b27654eacdf900c6b | The root secret is the password for the root user. If you start with an empty blockchain, the root user is needed to add other users, approve new nodes,.. If you don't set a value via the environment variable, the API generates one randomly and prints it to the console **Caution:** If you want to run TruBudget in production, make sure to set a secure root secret. Minimal value: 8. | +| **MULTICHAIN_RPC_HOST** | no | localhost | The IP address of the blockchain (not multichain daemon,but they are usally the same) you want to connect to. | +| **MULTICHAIN_RPC_PORT** | no | 8000 | The Port of the blockchain where the server is available for incoming http connections (e.g. readiness, versions, backup and restore) | +| **MULTICHAIN_PROTOCOL** | no | http | The protocol used to expose the multichain daemon of your Trubudget blockchain installation(bc). The protocol used to connect to the multichain daemon(api). This will be used internally for the communication between the API and the multichain daemon. Allowed values: http, https. | +| **MULTICHAIN_RPC_USER** | no | multichainrpc | The user used to connect to the multichain daemon. | +| **MULTICHAIN_RPC_PASSWORD** | no | s750SiJnj50yIrmwxPnEdSzpfGlTAHzhaUwgqKeb0G1j | Password used by the API to connect to the blockchain. The password is set by the origin node upon start. Every beta node needs to use the same RPC password in order to be able to connect to the blockchain. **Hint:** Although the MULTICHAIN_RPC_PASSWORD is not required it is highly recommended to set an own secure one. | +| **BLOCKCHAIN_PORT** | no | 8085 | The port used to expose the multichain daemon of your Trubudget blockchain installation(bc). The port used to connect to the multichain daemon(api). This will be used internally for the communication between the API and the multichain daemon. | +| **BLOCKCHAIN_PROTOCOL** | no | http | The Protocol of the blockchain where the server is available for incoming http connections. Allowed values: http, https. | +| **SWAGGER_BASEPATH** `deprecated` | no | - | deprecated This variable was used to choose which environment (prod or test) is used for testing the requests. The variable is deprecated now, as the Swagger documentation can be used for the prod and test environment separately. Example values: /. | +| **JWT_ALGORITHM** | no | HS256 | Algorithm used for signing and verifying JWTs. Allowed values: HS256, RS256. | +| **JWT_SECRET** | yes (if JWT_ALGORITHM=RS256) | a9be997c37b75ed93239bebeeb619a34d6032b9748d31d543654ed7cf5f1f849 | A string that is used to sign JWT which are created by the authenticate endpoint of the api. If JWT_ALGORITHM is set to `RS256`, this is required and holds BASE64 encoded PEM encoded private key for RSA. Minimal value: 32. | +| **JWT_PUBLIC_KEY** | yes (if JWT_ALGORITHM=RS256) | - | If JWT_ALGORITHM is set to `RS256`, this is required and holds BASE64 encoded PEM encoded public key for RSA. | +| **DOCUMENT_FEATURE_ENABLED** | no | - | If true, all uploaded documents are stored using trubudget's storage-service. If false, the document feature of TruBudget is disabled, and trying to upload a document will result in an error. | +| **DOCUMENT_EXTERNAL_LINKS_ENABLED** | no | - | If true, it is possible to use external documents links also without TruBudget's storage-service. If false, the external documents links feature of TruBudget is still possible to use in case DOCUMENT_FEATURE_ENABLED equals "true". | +| **STORAGE_SERVICE_HOST** | no | localhost | IP of connected storage service | +| **STORAGE_SERVICE_PORT** | no | 8090 | Port of connected storage service | +| **STORAGE_SERVICE_PROTOCOL** | no | http | Protocol of connected storage service. Allowed values: http, https. | +| **STORAGE_SERVICE_EXTERNAL_URL** | yes (if DOCUMENT_FEATURE_ENABLED=true) | - | IP and port of own connected storage service accessible externally | +| **EMAIL_HOST** | no | localhost | - | +| **EMAIL_PORT** | no | 8089 | - | +| **EMAIL_PROTOCOL** | no | http | Protocol of connected storage service. Allowed values: http, https. | +| **ACCESS_CONTROL_ALLOW_ORIGIN** | no | * | Since the service uses CORS, the domain by which it can be called needs to be set. Setting this value to `*` means that it can be called from any domain. Read more about this topic [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). | +| **NODE_ENV** | no | production | If set to `development` api will allow any string as password. If set to `production` passwords must satisfy safePasswordSchema, see lib/joiValidation-.ts & -.spec.ts files Allowed values: production, development, testing. | +| **ENCRYPTION_PASSWORD** | no | - | If set, all data that is send to the MultiChain node and external storage will be symmetrically encrypted by the ENCRYPTION_PASSWORD | +| **SIGNING_METHOD** | no | node | Possible signing methods are: `node` and `user`. Transactions on the chain will be signed using either the address of the node or the address of the specific user publishing that transaction. Allowed values: node, user. | +| **RATE_LIMIT** | no | - | If set, API will limit the number of requests from any individual IP address to the set number per minute. Can be set to any `number`, but shouldn't be set too low. Allowed values: . | +| **AUTHPROXY_ENABLED** | no | - | Enables logging in using the authorization token from authentication proxy | +| **AUTHPROXY_JWS_SIGNATURE** | yes (if AUTHPROXY_ENABLED=true) | - | secret/public key/certificate for verifying auth proxy token signature | +| **DB_TYPE** | no | pg | - | +| **SQL_DEBUG** | no | - | - | +| **REFRESH_TOKEN_STORAGE** | no | - | Determining the type of storage for refresh tokens. Allowed values are "db" or "memory" or blank to disable refresh token functionality. Allowed values: db, memory, . | +| **API_DB_USER** | yes (if REFRESH_TOKEN_STORAGE=db) | postgres | Database user for database connection, e.g. postgres | +| **API_DB_PASSWORD** | yes (if REFRESH_TOKEN_STORAGE=db) | test | Database password for database connection | +| **API_DB_HOST** | yes (if REFRESH_TOKEN_STORAGE=db) | localhost | Database host | +| **API_DB_NAME** | yes (if REFRESH_TOKEN_STORAGE=db) | trubudget_email_service | Name of the used database Example values: trubudget-db. | +| **API_DB_PORT** | yes (if REFRESH_TOKEN_STORAGE=db) | 5432 | Database port, e.g. 5432 | +| **API_DB_SSL** | yes (if REFRESH_TOKEN_STORAGE=db) | - | Database SSL connection. Allowed values: "true" or "false". | +| **API_DB_SCHEMA** | yes (if REFRESH_TOKEN_STORAGE=db) | public | Database schema, e.g. "public". | +| **API_REFRESH_TOKENS_TABLE** | yes (if REFRESH_TOKEN_STORAGE=db) | refresh_token | Name of table where refresh tokens will be stored, e.g. "refresh_token". | +| **SNAPSHOT_EVENT_INTERVAL** | no | 3 | - | +| **SILENCE_LOGGING_ON_FREQUENT_ROUTES** | no | - | Set to "true" if you want to hide route logging on frequent and technical endpoints like `/readiness`, `/version`, etc. | +| **APPLICATIONINSIGHTS_CONNECTION_STRING** | no | - | Azure Application Insights Connection String Allowed values: . | \ No newline at end of file diff --git a/api/package-lock.json b/api/package-lock.json index cb3a99cac..052582dbe 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -84,6 +84,19 @@ "typescript": "^5.0.4" } }, + "../scripts/common/envVarsGenerator": { + "name": "envvarsgenerator", + "version": "1.0.0", + "extraneous": true, + "dependencies": { + "joi": "^17.13.3" + }, + "devDependencies": { + "diff": "^7.0.0", + "rimraf": "^6.0.1", + "typescript": "^5.6.2" + } + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", diff --git a/api/package.json b/api/package.json index 552d13f71..dfd5dead1 100644 --- a/api/package.json +++ b/api/package.json @@ -39,7 +39,7 @@ "build": "rimraf dist && tsc", "dev": "ts-node-dev --poll --respawn -r tsconfig-paths/register src/index.ts -- --inspect=0.0.0.0:9229", "watch": "ts-node-dev --inspect=0.0.0.0:9229 --poll --respawn -r tsconfig-paths/register src/index.ts", - "test": "nyc mocha --require ts-node/register -r tsconfig-paths/register \"src/**/*.spec.ts\"", + "test": "ORGANIZATION=AcmeInc ORGANIZATION_VAULT_SECRET=AcmeIncSecret ROOT_SECRET=RootSecret nyc mocha --require ts-node/register -r tsconfig-paths/register \"src/**/*.spec.ts\"", "coverage": "nyc report --reporter=text-lcov", "generate-report": "nyc report --reporter=html", "lint": "eslint src --ext ts", @@ -47,7 +47,9 @@ "tsc": "tsc", "test:here": "node ./runTest.js", "generate-joi": "node dist/scripts/joiGenerator.js", - "build-docs": "bash ./buildDocs.sh" + "build-docs": "bash ./buildDocs.sh", + "validate-env-variables": "node ./dist/scripts/envVarsValidator.js", + "generate-env-vars-docs": "ts-node ./scripts/envVarsDocsGenerator.ts" }, "nyc": { "include": [ diff --git a/api/scripts/envVarsDocsGenerator.ts b/api/scripts/envVarsDocsGenerator.ts new file mode 100644 index 000000000..30c8226ae --- /dev/null +++ b/api/scripts/envVarsDocsGenerator.ts @@ -0,0 +1,17 @@ +import { writeFileSync } from "fs"; +import { generateMarkdownFile } from "../../scripts/common/envVarsGenerator/dist"; +import { envVarsSchema } from "../src/envVarsSchema"; + +function updateReadme(): void { + const mdTable = generateMarkdownFile(envVarsSchema); + + const md = `# TruBudget-API + +## Environment variables + +${mdTable}`; + + writeFileSync("./environment-variables.md", md, "utf-8"); +} + +updateReadme(); diff --git a/api/src/config.ts b/api/src/config.ts index c7b75315c..5ac96d6dd 100644 --- a/api/src/config.ts +++ b/api/src/config.ts @@ -1,5 +1,4 @@ -import logger from "./lib/logger"; -import { randomString } from "./service/hash"; +import { envVarsSchema } from "./envVarsSchema"; export interface JwtConfig { secretOrPrivateKey: string; @@ -18,9 +17,11 @@ interface ProcessEnvVars { ROOT_SECRET: string; MULTICHAIN_RPC_HOST: string; MULTICHAIN_RPC_PORT: string; + MULTICHAIN_RPC_PROTOCOL: "http" | "https"; MULTICHAIN_RPC_USER: string; MULTICHAIN_RPC_PASSWORD: string; BLOCKCHAIN_PORT: string; + BLOCKCHAIN_PROTOCOL: "http" | "https"; JWT_ALGORITHM: string; JWT_SECRET: string; JWT_PUBLIC_KEY: string; @@ -30,6 +31,7 @@ interface ProcessEnvVars { DOCUMENT_EXTERNAL_LINKS_ENABLED: string; STORAGE_SERVICE_HOST: string; STORAGE_SERVICE_PORT: string; + STORAGE_SERVICE_PROTOCOL: "http" | "https"; STORAGE_SERVICE_EXTERNAL_URL: string; EMAIL_HOST: string; EMAIL_PORT: string; @@ -127,138 +129,80 @@ interface Config { silenceLoggingOnFrequentRoutes: boolean; } -/** - * environment variables which are required by the API - * @notExported - */ -const requiredEnvVars = ["ORGANIZATION", "ORGANIZATION_VAULT_SECRET"]; +const { error, value: envVars } = envVarsSchema.validate(process.env); +if (error) { + throw new Error(`Config validation error: ${error.message}`); +} export const config: Config = { - organization: process.env.ORGANIZATION || "", - organizationVaultSecret: process.env.ORGANIZATION_VAULT_SECRET || "", - port: Number(process.env.PORT) || 8080, - rootSecret: process.env.ROOT_SECRET || randomString(32), + organization: envVars.ORGANIZATION, + organizationVaultSecret: envVars.ORGANIZATION_VAULT_SECRET, + port: envVars.PORT, + rootSecret: envVars.ROOT_SECRET, // RPC is the mutlichain daemon rpc: { - host: process.env.MULTICHAIN_RPC_HOST || "localhost", - port: Number(process.env.MULTICHAIN_RPC_PORT) || 8000, - protocol: process.env.MULTICHAIN_RPC_PROTOCOL === "https" ? "https" : "http", - user: process.env.MULTICHAIN_RPC_USER || "multichainrpc", - password: process.env.MULTICHAIN_RPC_PASSWORD || "s750SiJnj50yIrmwxPnEdSzpfGlTAHzhaUwgqKeb0G1j", + host: envVars.MULTICHAIN_RPC_HOST, + port: envVars.MULTICHAIN_RPC_PORT, + protocol: envVars.MULTICHAIN_RPC_PROTOCOL, + user: envVars.MULTICHAIN_RPC_USER, + password: envVars.MULTICHAIN_RPC_PASSWORD, }, // Blockchain is the blockchain component of Trubudget // It serves e.g. backup or version endpoints blockchain: { - host: process.env.MULTICHAIN_RPC_HOST || "localhost", - port: Number(process.env.BLOCKCHAIN_PORT) || 8085, - protocol: process.env.BLOCKCHAIN_PROTOCOL === "https" ? "https" : "http", + host: envVars.MULTICHAIN_RPC_HOST, + port: envVars.BLOCKCHAIN_PORT, + protocol: envVars.BLOCKCHAIN_PROTOCOL, }, jwt: { - secretOrPrivateKey: process.env.JWT_SECRET || randomString(32), - publicKey: process.env.JWT_PUBLIC_KEY || "", - algorithm: process.env.JWT_ALGORITHM === "RS256" ? "RS256" : "HS256", + secretOrPrivateKey: envVars.JWT_SECRET, + publicKey: envVars.JWT_PUBLIC_KEY, + algorithm: envVars.JWT_ALGORITHM, }, secureCookie: process.env.API_SECURE_COOKIE === "true" || process.env.NODE_ENV === "production", npmPackageVersion: process.env.npm_package_version || "", // Continues Integration ciCommitSha: process.env.CI_COMMIT_SHA || "", buildTimeStamp: process.env.BUILDTIMESTAMP || "", - documentFeatureEnabled: process.env.DOCUMENT_FEATURE_ENABLED === "true" ? true : false, - documentExternalLinksEnabled: - process.env.DOCUMENT_EXTERNAL_LINKS_ENABLED === "true" ? true : false, + documentFeatureEnabled: envVars.DOCUMENT_FEATURE_ENABLED, + documentExternalLinksEnabled: envVars.DOCUMENT_EXTERNAL_LINKS_ENABLED, storageService: { - host: process.env.STORAGE_SERVICE_HOST || "localhost", - port: Number(process.env.STORAGE_SERVICE_PORT) || 8090, - protocol: process.env.STORAGE_SERVICE_PROTOCOL === "https" ? "https" : "http", - externalUrl: process.env.STORAGE_SERVICE_EXTERNAL_URL || "", + host: envVars.STORAGE_SERVICE_HOST, + port: envVars.STORAGE_SERVICE_PORT, + protocol: envVars.STORAGE_SERVICE_PROTOCOL, + externalUrl: envVars.STORAGE_SERVICE_EXTERNAL_URL, }, emailService: { - host: process.env.EMAIL_HOST || "localhost", - port: Number(process.env.EMAIL_PORT) || 8089, - protocol: process.env.EMAIL_PROTOCOL === "https" ? "https" : "http", + host: envVars.EMAIL_HOST, + port: envVars.EMAIL_PORT, + protocol: envVars.EMAIL_PROTOCOL, }, - encryptionPassword: - process.env.ENCRYPTION_PASSWORD === "" ? undefined : process.env.ENCRYPTION_PASSWORD, - signingMethod: process.env.SIGNING_METHOD || "node", - nodeEnv: process.env.NODE_ENV || "production", - accessControlAllowOrigin: process.env.ACCESS_CONTROL_ALLOW_ORIGIN || "*", - rateLimit: - process.env.RATE_LIMIT === "" || isNaN(Number(process.env.RATE_LIMIT)) - ? undefined - : Number(process.env.RATE_LIMIT), + encryptionPassword: envVars.ENCRYPTION_PASSWORD, + signingMethod: envVars.SIGNING_METHOD, + nodeEnv: envVars.NODE_ENV, + accessControlAllowOrigin: envVars.ACCESS_CONTROL_ALLOW_ORIGIN, + rateLimit: envVars.RATE_LIMIT, authProxy: { - enabled: process.env.AUTHPROXY_ENABLED === "true" || false, + enabled: envVars.AUTHPROXY_ENABLED, authProxyCookie: "authorizationToken", - jwsSignature: process.env.AUTHPROXY_JWS_SIGNATURE || undefined, + jwsSignature: envVars.AUTHPROXY_JWS_SIGNATURE, }, db: { - user: process.env.API_DB_USER || "postgres", - password: process.env.API_DB_PASSWORD || "test", - host: process.env.API_DB_HOST || "localhost", - database: process.env.API_DB_DATABASE || "trubudget_email_service", - port: Number(process.env.API_DB_PORT) || 5432, - ssl: process.env.API_DB_SSL === "true", - schema: process.env.API_DB_SCHEMA || "public", + user: envVars.API_DB_USER, + password: envVars.API_DB_PASSWORD, + host: envVars.API_DB_HOST, + database: envVars.API_DB_NAME, + port: envVars.API_DB_PORT, + ssl: envVars.API_DB_SSL, + schema: envVars.API_DB_SCHEMA, }, - dbType: process.env.DB_TYPE || "pg", - sqlDebug: Boolean(process.env.SQL_DEBUG) || false, - refreshTokensTable: process.env.API_REFRESH_TOKENS_TABLE || "refresh_token", - refreshTokenStorage: - process.env.REFRESH_TOKEN_STORAGE && - ["db", "memory"].includes(process.env.REFRESH_TOKEN_STORAGE) - ? process.env.REFRESH_TOKEN_STORAGE - : undefined, - snapshotEventInterval: Number(process.env.SNAPSHOT_EVENT_INTERVAL) || 3, - azureMonitorConnectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING || "", - silenceLoggingOnFrequentRoutes: - process.env.SILENCE_LOGGING_ON_FREQUENT_ROUTES === "true" || false, -}; - -/** - * Checks if required environment variables are set, stops the process otherwise - * @notExported - * - * @param requiredEnvVars environment variables required for the API to run - */ -function exitIfMissing(requiredEnvVars): void { - let envVarMissing = false; - requiredEnvVars.forEach((env) => { - if (!envExists(process.env, env)) envVarMissing = true; - }); - if (envVarMissing) process.exit(1); -} - -/** - * Checks if an environment variable is attached to the current process - * @notExported - * - * @param processEnv environment variables attached to the current process - * @param prop environment variable to check - * @param msg optional message to print out - * @returns a boolean indicating if an environment variable is attached to the current process - */ -const envExists = ( - processEnv: Partial, - prop: K, - msg?: string, -): boolean => { - if (processEnv[prop] === undefined || processEnv[prop] === null) { - switch (prop) { - case "ORGANIZATION": - msg = "Please set ORGANIZATION to the organization this node belongs to."; - break; - case "ORGANIZATION_VAULT_SECRET": - msg = - "Please set ORGANIZATION_VAULT_SECRET to the secret key used to encrypt the organization's vault."; - break; - default: - break; - } - logger.fatal(msg || `Environment is missing required variable ${String(prop)}`); - return false; - } else { - return true; - } + dbType: envVars.DB_TYPE, + sqlDebug: envVars.SQL_DEBUG, + refreshTokensTable: envVars.API_REFRESH_TOKENS_TABLE, + refreshTokenStorage: envVars.REFRESH_TOKEN_STORAGE, + snapshotEventInterval: envVars.SNAPSHOT_EVENT_INTERVAL, + azureMonitorConnectionString: envVars.APPLICATIONINSIGHTS_CONNECTION_STRING, + silenceLoggingOnFrequentRoutes: envVars.SILENCE_LOGGING_ON_FREQUENT_ROUTES, }; /** @@ -268,37 +212,6 @@ const envExists = ( * @notExported */ const getValidConfig = (): Config => { - exitIfMissing(requiredEnvVars); - - // Environment Validation - const jwtSecret: string = process.env.JWT_SECRET || randomString(32); - if (jwtSecret.length < 32) { - logger.warn("Warning: the JWT secret key should be at least 32 characters long."); - } - const rootSecret: string = process.env.ROOT_SECRET || randomString(32); - if (!process.env.ROOT_SECRET) { - logger.warn(`Warning: root password not set; autogenerated to ${rootSecret}`); - } - - // Document feature enabled - if (process.env.DOCUMENT_FEATURE_ENABLED === "true") { - const requiredDocEnvVars = ["STORAGE_SERVICE_EXTERNAL_URL"]; - exitIfMissing(requiredDocEnvVars); - } - - const jwtAlgorithm: string = process.env.JWT_ALGORITHM; - if ( - !( - jwtAlgorithm === "HS256" || - jwtAlgorithm === "RS256" || - jwtAlgorithm === undefined || - jwtAlgorithm === "" - ) - ) { - logger.fatal("JWT_ALGORITHM must be either HS256 or RS256 or empty (defaults to HS256)"); - process.exit(1); - } - return config; }; /** diff --git a/api/src/envVarsSchema.ts b/api/src/envVarsSchema.ts new file mode 100644 index 000000000..6d4f6adb2 --- /dev/null +++ b/api/src/envVarsSchema.ts @@ -0,0 +1,240 @@ +import * as Joi from "joi"; + +import { randomString } from "./service/hash"; + +export const envVarsSchema = Joi.object({ + LOG_LEVEL: Joi.string() + .default("info") + .allow("trace", "debug", "info", "warn", "error", "fatal") + .note("Defines the log output."), + ORGANIZATION: Joi.string() + .min(1) + .max(100) + .required() + .note( + "In the blockchain network, each node is represented by its organization name. This environment variable sets this organization name. It is used to create the organization stream on the blockchain and is also displayed in the frontend's top right corner.", + ), + PORT: Joi.number() + .port() + .default(8091) + .note( + "The port used to expose the API for your installation.
Example: If you run TruBudget locally and set API_PORT to `8080`, you can reach the API via `localhost:8080/api`.", + ), + ORGANIZATION_VAULT_SECRET: Joi.string() + .invalid("secret") + .required() + .note( + "This is the key to en-/decrypt user data of an organization. If you want to add a new node for your organization, you want users to be able to log in on either node.
**Caution:** If you want to run TruBudget in production, make sure NOT to use the default value from the `.env_example` file!", + ), + ROOT_SECRET: Joi.string() + .min(8) + .required() + .default(randomString(32)) + .note( + "The root secret is the password for the root user. If you start with an empty blockchain, the root user is needed to add other users, approve new nodes,.. If you don't set a value via the environment variable, the API generates one randomly and prints it to the console
**Caution:** If you want to run TruBudget in production, make sure to set a secure root secret.", + ), + MULTICHAIN_RPC_HOST: Joi.string() + .default("localhost") + .note( + "The IP address of the blockchain (not multichain daemon,but they are usally the same) you want to connect to.", + ), + MULTICHAIN_RPC_PORT: Joi.number() + .default(8000) + .note( + "The Port of the blockchain where the server is available for incoming http connections (e.g. readiness, versions, backup and restore)", + ), + MULTICHAIN_PROTOCOL: Joi.string() + .default("http") + .allow("http", "https") + .note( + "The protocol used to expose the multichain daemon of your Trubudget blockchain installation(bc). The protocol used to connect to the multichain daemon(api). This will be used internally for the communication between the API and the multichain daemon.", + ), + MULTICHAIN_RPC_USER: Joi.string() + .default("multichainrpc") + .note("The user used to connect to the multichain daemon."), + MULTICHAIN_RPC_PASSWORD: Joi.string() + .default("s750SiJnj50yIrmwxPnEdSzpfGlTAHzhaUwgqKeb0G1j") + .note( + "Password used by the API to connect to the blockchain. The password is set by the origin node upon start. Every beta node needs to use the same RPC password in order to be able to connect to the blockchain.
**Hint:** Although the MULTICHAIN_RPC_PASSWORD is not required it is highly recommended to set an own secure one.", + ), + BLOCKCHAIN_PORT: Joi.number() + .default(8085) + .note( + "The port used to expose the multichain daemon of your Trubudget blockchain installation(bc). The port used to connect to the multichain daemon(api). This will be used internally for the communication between the API and the multichain daemon.", + ), + BLOCKCHAIN_PROTOCOL: Joi.string() + .default("http") + .allow("http", "https") + .note( + "The Protocol of the blockchain where the server is available for incoming http connections.", + ), + SWAGGER_BASEPATH: Joi.string() + .example("/") + .forbidden() + .note( + "deprecated", + "This variable was used to choose which environment (prod or test) is used for testing the requests. The variable is deprecated now, as the Swagger documentation can be used for the prod and test environment separately.", + ), + JWT_ALGORITHM: Joi.string() + .default("HS256") + .valid("HS256", "RS256") + .note("Algorithm used for signing and verifying JWTs."), + JWT_SECRET: Joi.string() + .min(10) + .default(randomString(10)) + .when("JWT_ALGORITHM", { + is: "RS256", + then: Joi.string().base64().required(), + }) + .note( + "A string that is used to sign JWT which are created by the authenticate endpoint of the api. If JWT_ALGORITHM is set to `RS256`, this is required and holds BASE64 encoded PEM encoded private key for RSA.", + ), + JWT_PUBLIC_KEY: Joi.string() + .default("") + .when("JWT_ALGORITHM", { + is: "RS256", + then: Joi.string().base64().required(), + }) + .note( + "If JWT_ALGORITHM is set to `RS256`, this is required and holds BASE64 encoded PEM encoded public key for RSA.", + ), + DOCUMENT_FEATURE_ENABLED: Joi.boolean() + .default(false) + .note( + "If true, all uploaded documents are stored using trubudget's storage-service. If false, the document feature of TruBudget is disabled, and trying to upload a document will result in an error.", + ), + DOCUMENT_EXTERNAL_LINKS_ENABLED: Joi.boolean() + .default(false) + .note( + 'If true, it is possible to use external documents links also without TruBudget\'s storage-service. If false, the external documents links feature of TruBudget is still possible to use in case DOCUMENT_FEATURE_ENABLED equals "true".', + ), + STORAGE_SERVICE_HOST: Joi.string().default("localhost").note("IP of connected storage service"), + STORAGE_SERVICE_PORT: Joi.number().default(8090).note("Port of connected storage service"), + STORAGE_SERVICE_PROTOCOL: Joi.string() + .default("http") + .allow("http", "https") + .note("Protocol of connected storage service."), + STORAGE_SERVICE_EXTERNAL_URL: Joi.string() + .default("") + .when("DOCUMENT_FEATURE_ENABLED", { + is: true, + then: Joi.required(), + }) + .note("IP and port of own connected storage service accessible externally"), + EMAIL_HOST: Joi.string().default("localhost"), + EMAIL_PORT: Joi.number().default(8089), + EMAIL_PROTOCOL: Joi.string() + .default("http") + .allow("http", "https") + .note("Protocol of connected storage service."), + ACCESS_CONTROL_ALLOW_ORIGIN: Joi.string() + .default("*") + .note( + "Since the service uses CORS, the domain by which it can be called needs to be set. Setting this value to `*` means that it can be called from any domain. Read more about this topic [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS).", + ), + NODE_ENV: Joi.string() + .default("production") + .allow("production", "development", "testing") + .note( + "If set to `development` api will allow any string as password. If set to `production` passwords must satisfy safePasswordSchema, see lib/joiValidation-.ts & -.spec.ts files", + ), + ENCRYPTION_PASSWORD: Joi.string().note( + "If set, all data that is send to the MultiChain node and external storage will be symmetrically encrypted by the ENCRYPTION_PASSWORD", + ), + SIGNING_METHOD: Joi.string() + .default("node") + .allow("node", "user") + .note( + "Possible signing methods are: `node` and `user`. Transactions on the chain will be signed using either the address of the node or the address of the specific user publishing that transaction.", + ), + RATE_LIMIT: Joi.number() + .allow("") + .empty("") + .note( + "If set, API will limit the number of requests from any individual IP address to the set number per minute. Can be set to any `number`, but shouldn't be set too low.", + ), + AUTHPROXY_ENABLED: Joi.boolean() + .default(false) + .note("Enables logging in using the authorization token from authentication proxy"), + AUTHPROXY_JWS_SIGNATURE: Joi.string() + .when("AUTHPROXY_ENABLED", { + is: true, + then: Joi.required(), + }) + .note("secret/public key/certificate for verifying auth proxy token signature"), + DB_TYPE: Joi.string().default("pg"), + SQL_DEBUG: Joi.boolean().default(false), + API_DB_USER: Joi.string() + .default("postgres") + .when("REFRESH_TOKEN_STORAGE", { + is: "db", + then: Joi.required(), + }) + .note("Database user for database connection, e.g. postgres"), + API_DB_PASSWORD: Joi.string() + .when("REFRESH_TOKEN_STORAGE", { + is: "db", + then: Joi.required(), + }) + .default("test") + .note("Database password for database connection"), + API_DB_HOST: Joi.string() + .when("REFRESH_TOKEN_STORAGE", { + is: "db", + then: Joi.required(), + }) + .default("localhost") + .note("Database host"), + API_DB_NAME: Joi.string() + .when("REFRESH_TOKEN_STORAGE", { + is: "db", + then: Joi.required(), + }) + .default("trubudget_email_service") + .example("trubudget-db") + .note("Name of the used database"), + API_DB_PORT: Joi.number() + .when("REFRESH_TOKEN_STORAGE", { + is: "db", + then: Joi.required(), + }) + .default(5432) + .note("Database port, e.g. 5432"), + API_DB_SSL: Joi.boolean() + .when("REFRESH_TOKEN_STORAGE", { + is: "db", + then: Joi.required(), + }) + .default(false) + .note('Database SSL connection. Allowed values: "true" or "false".'), + API_DB_SCHEMA: Joi.string() + .when("REFRESH_TOKEN_STORAGE", { + is: "db", + then: Joi.required(), + }) + .default("public") + .note('Database schema, e.g. "public".'), + API_REFRESH_TOKENS_TABLE: Joi.string() + .default("refresh_token") + .when("REFRESH_TOKEN_STORAGE", { + is: "db", + then: Joi.required(), + }) + .note('Name of table where refresh tokens will be stored, e.g. "refresh_token".'), + REFRESH_TOKEN_STORAGE: Joi.string() + .allow("db", "memory", "") + .note( + 'Determining the type of storage for refresh tokens. Allowed values are "db" or "memory" or blank to disable refresh token functionality.', + ), + SNAPSHOT_EVENT_INTERVAL: Joi.number().default(3), + SILENCE_LOGGING_ON_FREQUENT_ROUTES: Joi.boolean() + .default(false) + .note( + 'Set to "true" if you want to hide route logging on frequent and technical endpoints like `/readiness`, `/version`, etc.', + ), + APPLICATIONINSIGHTS_CONNECTION_STRING: Joi.string() + .allow("") + .note("Azure Application Insights Connection String"), +}) + .unknown() + .required(); diff --git a/api/src/lib/joiValidation.spec.ts b/api/src/lib/joiValidation.spec.ts index 477d42171..50ad9fa65 100644 --- a/api/src/lib/joiValidation.spec.ts +++ b/api/src/lib/joiValidation.spec.ts @@ -1,18 +1,18 @@ import { expect } from "chai"; import Joi = require("joi"); -import { safeIdSchema, safePasswordSchema, safeStringSchema } from "./joiValidation"; +import { safeIdSchema, safePasswordSchemaProduction, safeStringSchema } from "./joiValidation"; const safeStringSchemaSchema = Joi.alternatives([safeStringSchema]); const safeIdSchemaSchema = Joi.alternatives([safeIdSchema]); -const safePasswordSchemaSchema = Joi.alternatives([safePasswordSchema]); +const safePasswordSchemaSchemaProduction = Joi.alternatives([safePasswordSchemaProduction]); describe("JoiValidation: Password", () => { it("Should accept user creation with a correct Password", async () => { const pass = "Test1234"; const controlValue = "Test1234"; - const { value, error } = safePasswordSchemaSchema.validate(pass); + const { value, error } = safePasswordSchemaSchemaProduction.validate(pass); expect(value).to.equal(controlValue); expect(error).to.equal(undefined); @@ -21,7 +21,7 @@ describe("JoiValidation: Password", () => { it("Should not accept a weak Password (only in Production)", async () => { const pass = "asdfasdf"; - const { value, error } = safePasswordSchemaSchema.validate(pass); + const { value, error } = safePasswordSchemaSchemaProduction.validate(pass); expect(value).to.equal(undefined); expect(error?.message || "").to.contain("fails to match the required pattern:"); @@ -30,7 +30,7 @@ describe("JoiValidation: Password", () => { it("Should not accept a short Password", async () => { const pass = "Test123"; - const { value, error } = safePasswordSchemaSchema.validate(pass); + const { value, error } = safePasswordSchemaSchemaProduction.validate(pass); expect(value).to.equal(undefined); expect(error?.message || "").to.contain("length must be at least 8 characters long"); @@ -39,7 +39,7 @@ describe("JoiValidation: Password", () => { it("Should not accept a Password with malicious code", async () => { const pass = "Test1234