Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1953 joi env validation #1963

Merged
merged 23 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 49 additions & 49 deletions api/environment-variables.md

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions api/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,17 @@
"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",
"format": "prettier --write \"src/**/*.+(js|jsx|ts|tsx|yaml|mjs)\"",
"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": [
Expand Down
17 changes: 17 additions & 0 deletions api/scripts/envVarsDocsGenerator.ts
Original file line number Diff line number Diff line change
@@ -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();
193 changes: 53 additions & 140 deletions api/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import logger from "./lib/logger";
import { randomString } from "./service/hash";
import { envVarsSchema } from "./envVarsSchema";

export interface JwtConfig {
secretOrPrivateKey: string;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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 = <T, K extends keyof T>(
processEnv: Partial<T>,
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,
};

/**
Expand All @@ -268,37 +212,6 @@ const envExists = <T, K extends keyof T>(
* @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;
};
/**
Expand Down
Loading