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

TechDebt: API: Environment Variables Validation #1425

Merged
merged 11 commits into from
Nov 14, 2024
12 changes: 8 additions & 4 deletions api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,24 @@
} from './middleware/critterbase-proxy';
import { rootAPIDoc } from './openapi/root-api-doc';
import { authenticateRequest, authenticateRequestOptional } from './request-handlers/security/authentication';
import { loadEvironmentVariables } from './utils/env-config';
import { scanFileForVirus } from './utils/file-utils';
import { getLogger } from './utils/logger';

// Load and validate the environment variables
loadEvironmentVariables();

Check warning on line 21 in api/src/app.ts

View check run for this annotation

Codecov / codecov/patch

api/src/app.ts#L21

Added line #L21 was not covered by tests
NickPhura marked this conversation as resolved.
Show resolved Hide resolved

const defaultLog = getLogger('app');

const HOST = process.env.API_HOST;
const PORT = Number(process.env.API_PORT);
const PORT = process.env.API_PORT;

Check warning on line 26 in api/src/app.ts

View check run for this annotation

Codecov / codecov/patch

api/src/app.ts#L26

Added line #L26 was not covered by tests

// Max size of the body of the request (bytes)
const MAX_REQ_BODY_SIZE = Number(process.env.MAX_REQ_BODY_SIZE) || 52428800;
const MAX_REQ_BODY_SIZE = process.env.MAX_REQ_BODY_SIZE;

Check warning on line 29 in api/src/app.ts

View check run for this annotation

Codecov / codecov/patch

api/src/app.ts#L29

Added line #L29 was not covered by tests
// Max number of files in a single request
const MAX_UPLOAD_NUM_FILES = Number(process.env.MAX_UPLOAD_NUM_FILES) || 10;
const MAX_UPLOAD_NUM_FILES = process.env.MAX_UPLOAD_NUM_FILES;

Check warning on line 31 in api/src/app.ts

View check run for this annotation

Codecov / codecov/patch

api/src/app.ts#L31

Added line #L31 was not covered by tests
// Max size of a single file (bytes)
const MAX_UPLOAD_FILE_SIZE = Number(process.env.MAX_UPLOAD_FILE_SIZE) || 52428800;
const MAX_UPLOAD_FILE_SIZE = process.env.MAX_UPLOAD_FILE_SIZE;

Check warning on line 33 in api/src/app.ts

View check run for this annotation

Codecov / codecov/patch

api/src/app.ts#L33

Added line #L33 was not covered by tests

// Get initial express app
const app: express.Express = express();
Expand Down
8 changes: 4 additions & 4 deletions api/src/database/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ import { asyncErrorWrapper, getGenericizedKeycloakUserInformation, syncErrorWrap
const defaultLog = getLogger('database/db');

const getDbHost = () => process.env.DB_HOST;
const getDbPort = () => Number(process.env.DB_PORT);
const getDbPort = () => process.env.DB_PORT;
const getDbUsername = () => process.env.DB_USER_API;
const getDbPassword = () => process.env.DB_USER_API_PASS;
const getDbDatabase = () => process.env.DB_DATABASE;

const DB_POOL_SIZE: number = Number(process.env.DB_POOL_SIZE) || 20;
const DB_CONNECTION_TIMEOUT: number = Number(process.env.DB_CONNECTION_TIMEOUT) || 0;
const DB_IDLE_TIMEOUT: number = Number(process.env.DB_IDLE_TIMEOUT) || 10000;
const DB_POOL_SIZE = 20;
const DB_CONNECTION_TIMEOUT = 0;
const DB_IDLE_TIMEOUT = 10000;

export const DB_CLIENT = 'pg';

Expand Down
122 changes: 122 additions & 0 deletions api/src/utils/env-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { z } from 'zod';
import { getLogger } from './logger';

const defaultLog = getLogger('src/utils/env-config.ts');

Check warning on line 4 in api/src/utils/env-config.ts

View check run for this annotation

Codecov / codecov/patch

api/src/utils/env-config.ts#L4

Added line #L4 was not covered by tests

const ZodEnvString = z.string().trim().min(1, { message: 'Required' }); // '' or ' ' are invalid
const ZodEnvNumber = z.coerce.number().min(1, { message: 'Required and must be a positive value.' }); // -1 is invalid

Check warning on line 7 in api/src/utils/env-config.ts

View check run for this annotation

Codecov / codecov/patch

api/src/utils/env-config.ts#L6-L7

Added lines #L6 - L7 were not covered by tests

// Schema for environment configuration
export const EnvSchema = z.object({

Check warning on line 10 in api/src/utils/env-config.ts

View check run for this annotation

Codecov / codecov/patch

api/src/utils/env-config.ts#L10

Added line #L10 was not covered by tests
// Environment
NODE_ENV: z.enum(['development', 'test', 'production']),
NODE_OPTIONS: ZodEnvString,
TZ: z.literal('America/Vancouver'),

// API server
API_HOST: ZodEnvString,
API_PORT: ZodEnvNumber,

// Database
DB_HOST: ZodEnvString,
DB_PORT: ZodEnvNumber,
DB_USER_API: ZodEnvString,
DB_USER_API_PASS: ZodEnvString,
DB_DATABASE: ZodEnvString,

// Keycloak
KEYCLOAK_HOST: ZodEnvString,
KEYCLOAK_REALM: ZodEnvString,
KEYCLOAK_ADMIN_USERNAME: ZodEnvString,
KEYCLOAK_ADMIN_PASSWORD: ZodEnvString,
KEYCLOAK_API_TOKEN_URL: ZodEnvString,
KEYCLOAK_API_CLIENT_ID: ZodEnvString,
KEYCLOAK_API_CLIENT_SECRET: ZodEnvString,
KEYCLOAK_API_HOST: ZodEnvString,
KEYCLOAK_API_ENVIRONMENT: ZodEnvString,

// Logging
LOG_LEVEL: z.enum(['silent', 'error', 'warn', 'info', 'debug', 'silly']),
LOG_LEVEL_FILE: z.enum(['silent', 'error', 'warn', 'info', 'debug', 'silly']),
LOG_FILE_DIR: ZodEnvString,
LOG_FILE_NAME: ZodEnvString,
LOG_FILE_DATE_PATTERN: ZodEnvString,
LOG_FILE_MAX_SIZE: ZodEnvString,
LOG_FILE_MAX_FILES: ZodEnvString,

// Validation
API_RESPONSE_VALIDATION_ENABLED: z.enum(['true', 'false']),
DATABASE_RESPONSE_VALIDATION_ENABLED: z.enum(['true', 'false']),

// File upload limits
MAX_REQ_BODY_SIZE: ZodEnvNumber,
MAX_UPLOAD_NUM_FILES: ZodEnvNumber,
MAX_UPLOAD_FILE_SIZE: ZodEnvNumber,

// External Services
CB_API_HOST: ZodEnvString,
APP_HOST: ZodEnvString,

// Biohub
BACKBONE_INTERNAL_API_HOST: ZodEnvString,
BACKBONE_PUBLIC_API_HOST: ZodEnvString,
BACKBONE_INTAKE_PATH: ZodEnvString,
BACKBONE_ARTIFACT_INTAKE_PATH: ZodEnvString,
BIOHUB_TAXON_PATH: ZodEnvString,
BIOHUB_TAXON_TSN_PATH: ZodEnvString,

// Object Store
OBJECT_STORE_URL: ZodEnvString,
OBJECT_STORE_ACCESS_KEY_ID: ZodEnvString,
OBJECT_STORE_SECRET_KEY_ID: ZodEnvString,
OBJECT_STORE_BUCKET_NAME: ZodEnvString,
S3_KEY_PREFIX: ZodEnvString,

// GCNotify
GCNOTIFY_SECRET_API_KEY: ZodEnvString,
GCNOTIFY_ADMIN_EMAIL: ZodEnvString,
GCNOTIFY_ONBOARDING_REQUEST_EMAIL_TEMPLATE: z.string().uuid(),
GCNOTIFY_ONBOARDING_REQUEST_SMS_TEMPLATE: z.string().uuid(),
GCNOTIFY_REQUEST_RESUBMIT_TEMPLATE: z.string().uuid(),
GCNOTIFY_EMAIL_URL: ZodEnvString,
GCNOTIFY_SMS_URL: ZodEnvString,

// ClamAV
CLAMAV_PORT: ZodEnvNumber,
CLAMAV_HOST: ZodEnvString,
ENABLE_FILE_VIRUS_SCAN: z.enum(['true', 'false']),

// Extra
FEATURE_FLAGS: z.string().trim().optional() // flagA,flagB,flagC
});

type Env = z.infer<typeof EnvSchema>;

/**
* Load Environment Variables and validate them against the Zod schema.
*
* @returns {*} {Env} Validated environment variables
*/
export const loadEvironmentVariables = (): Env => {
const parsed = EnvSchema.safeParse(process.env);

Check warning on line 101 in api/src/utils/env-config.ts

View check run for this annotation

Codecov / codecov/patch

api/src/utils/env-config.ts#L100-L101

Added lines #L100 - L101 were not covered by tests

if (!parsed.success) {
defaultLog.error({

Check warning on line 104 in api/src/utils/env-config.ts

View check run for this annotation

Codecov / codecov/patch

api/src/utils/env-config.ts#L104

Added line #L104 was not covered by tests
label: 'loadEvironmentVariables',
message: 'Environment variables validation check failed',
errors: parsed.error.flatten().fieldErrors
});

process.exit(1);

Check warning on line 110 in api/src/utils/env-config.ts

View check run for this annotation

Codecov / codecov/patch

api/src/utils/env-config.ts#L110

Added line #L110 was not covered by tests
}

return parsed.data;

Check warning on line 113 in api/src/utils/env-config.ts

View check run for this annotation

Codecov / codecov/patch

api/src/utils/env-config.ts#L113

Added line #L113 was not covered by tests
};

// Extend NodeJS ProcessEnv to include the EnvSchema
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace NodeJS {
interface ProcessEnv extends Env {}
}
}
11 changes: 5 additions & 6 deletions api/src/utils/file-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,7 @@ describe('getS3HostUrl', () => {
});

it('should yield a default S3 host url', () => {
delete process.env.OBJECT_STORE_URL;
delete process.env.OBJECT_STORE_BUCKET_NAME;
Object.assign(process.env, { OBJECT_STORE_URL: undefined, OBJECT_STORE_BUCKET_NAME: undefined });

const result = getS3HostUrl();

Expand Down Expand Up @@ -193,7 +192,7 @@ describe('_getClamAvScanner', () => {
it('should return a clamAv scanner client', () => {
process.env.ENABLE_FILE_VIRUS_SCAN = 'true';
process.env.CLAMAV_HOST = 'host';
process.env.CLAMAV_PORT = '1111';
process.env.CLAMAV_PORT = 1111;

const result = _getClamAvScanner();
expect(result).to.not.be.null;
Expand All @@ -215,7 +214,7 @@ describe('_getObjectStoreBucketName', () => {
});

it('should return its default value', () => {
delete process.env.OBJECT_STORE_BUCKET_NAME;
Object.assign(process.env, { OBJECT_STORE_BUCKET_NAME: undefined });

const result = _getObjectStoreBucketName();
expect(result).to.equal('');
Expand Down Expand Up @@ -251,7 +250,7 @@ describe('_getObjectStoreUrl', () => {
});

it('should return its default value', () => {
delete process.env.OBJECT_STORE_URL;
Object.assign(process.env, { OBJECT_STORE_URL: undefined });

const result = _getObjectStoreUrl();
expect(result).to.equal('https://nrs.objectstore.gov.bc.ca');
Expand All @@ -273,7 +272,7 @@ describe('getS3KeyPrefix', () => {
});

it('should return its default value', () => {
delete process.env.S3_KEY_PREFIX;
Object.assign(process.env, { S3_KEY_PREFIX: undefined });

const result = getS3KeyPrefix();
expect(result).to.equal('sims');
Expand Down
2 changes: 1 addition & 1 deletion api/src/utils/file-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const _getClamAvScanner = async (): Promise<NodeClam> => {
return new NodeClam().init({
clamdscan: {
host: process.env.CLAMAV_HOST,
port: Number(process.env.CLAMAV_PORT)
port: process.env.CLAMAV_PORT
}
});
};
Expand Down
6 changes: 0 additions & 6 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,6 @@ services:
- DB_PORT=5432
- DB_DATABASE=${DB_DATABASE}
- DB_SCHEMA=${DB_SCHEMA}
# Seed
- PROJECT_SEEDER_USER_IDENTIFIER=${PROJECT_SEEDER_USER_IDENTIFIER}
- NUM_SEED_PROJECTS=${NUM_SEED_PROJECTS}
- NUM_SEED_SURVEYS_PER_PROJECT=${NUM_SEED_SURVEYS_PER_PROJECT}
- NUM_SEED_OBSERVATIONS_PER_SURVEY=${NUM_SEED_OBSERVATIONS_PER_SURVEY}
- NUM_SEED_SUBCOUNTS_PER_OBSERVATION=${NUM_SEED_SUBCOUNTS_PER_OBSERVATION}
# Keycloak
- KEYCLOAK_HOST=${KEYCLOAK_HOST}
- KEYCLOAK_REALM=${KEYCLOAK_REALM}
Expand Down
Loading