Skip to content

Commit

Permalink
Merge pull request #20 from OpenNBS/feature/env-validation
Browse files Browse the repository at this point in the history
feat: implement environment variable validation with class-validator and class-transformer
  • Loading branch information
Bentroen authored Dec 20, 2024
2 parents 4924b3e + a1b5eb2 commit 2742271
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 46 deletions.
50 changes: 4 additions & 46 deletions server/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import * as fs from 'fs';

import { Logger, Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { MongooseModule, MongooseModuleFactoryOptions } from '@nestjs/mongoose';

import { validate } from 'class-validator';
import { AuthModule } from './auth/auth.module';
import { FileModule } from './file/file.module';
import { ParseTokenPipe } from './parseToken';
import { SongModule } from './song/song.module';
import { SongBrowserModule } from './song-browser/song-browser.module';
import { SongModule } from './song/song.module';
import { UserModule } from './user/user.module';

@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env.development', '.env.production'],
validate: validate,
}),
//DatabaseModule,
MongooseModule.forRootAsync({
Expand Down Expand Up @@ -44,46 +44,4 @@ import { UserModule } from './user/user.module';
providers: [ParseTokenPipe],
exports: [ParseTokenPipe],
})
export class AppModule {
private readonly logger = new Logger(AppModule.name);
constructor(private readonly configService: ConfigService) {
// read .env.development.example file
const file = '.env.development.example';
const encoding = 'utf8';
const fileData = fs.readFileSync(file, encoding);

const variableToIgnore = ['APP_DOMAIN', 'NODE_ENV', 'WHITELISTED_USERS'];

const variables = fileData
.split('\n')
// trim whitespace
.map((line) => line.trim())
// remove empty lines
.filter((line) => line.length > 0)
// get variable names
.map((line) => line.split('=')[0])
// remove variables that are not in the .env.development.example file
.filter((variable) => !variableToIgnore.includes(variable));

this.logger.warn(`Ignoring variables: ${variableToIgnore.join(', ')}`);
this.logger.warn(`Checking variables: ${variables.join(', ')}`);

let isMissing = false;

for (const variable of variables) {
const value = this.configService.get(variable);

if (!value) {
this.logger.error(
`Missing environment variable ${variable} in env vars}`,
);

isMissing = true;
}
}

if (isMissing) {
throw new Error('Missing environment variables in env vars file');
}
}
}
export class AppModule {}
100 changes: 100 additions & 0 deletions server/src/config/EnvironmentVariables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { plainToInstance } from 'class-transformer';
import { IsEnum, IsOptional, IsString, validateSync } from 'class-validator';

enum Environment {
Development = 'development',
Production = 'production',
}

export class EnvironmentVariables {
@IsEnum(Environment)
@IsOptional()
NODE_ENV?: Environment;

@IsString()
GITHUB_CLIENT_ID: string;

@IsString()
GITHUB_CLIENT_SECRET: string;

@IsString()
GOOGLE_CLIENT_ID: string;

@IsString()
GOOGLE_CLIENT_SECRET: string;

@IsString()
DISCORD_CLIENT_ID: string;

@IsString()
DISCORD_CLIENT_SECRET: string;

@IsString()
JWT_SECRET: string;

@IsString()
JWT_EXPIRES_IN: string;

@IsString()
JWT_REFRESH_SECRET: string;

@IsString()
JWT_REFRESH_EXPIRES_IN: string;

@IsString()
MONGO_URL: string;

@IsString()
SERVER_URL: string;

@IsString()
FRONTEND_URL: string;

@IsString()
@IsOptional()
APP_DOMAIN?: string;

@IsString()
RECAPTCHA_KEY: string;

@IsString()
S3_ENDPOINT: string;

@IsString()
S3_BUCKET_SONGS: string;

@IsString()
S3_BUCKET_THUMBS: string;

@IsString()
S3_KEY: string;

@IsString()
S3_SECRET: string;

@IsString()
S3_REGION: string;

@IsString()
@IsOptional()
WHITELISTED_USERS?: string;

@IsString()
DISCORD_WEBHOOK_URL: string;
}

export function validate(config: Record<string, unknown>) {
const validatedConfig = plainToInstance(EnvironmentVariables, config, {
enableImplicitConversion: true,
});

const errors = validateSync(validatedConfig, {
skipMissingProperties: false,
});

if (errors.length > 0) {
throw new Error(errors.toString());
}

return validatedConfig;
}

0 comments on commit 2742271

Please sign in to comment.