Skip to content

Commit 2742271

Browse files
authored
Merge pull request #20 from OpenNBS/feature/env-validation
feat: implement environment variable validation with class-validator and class-transformer
2 parents 4924b3e + a1b5eb2 commit 2742271

File tree

2 files changed

+104
-46
lines changed

2 files changed

+104
-46
lines changed

server/src/app.module.ts

Lines changed: 4 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
1-
import * as fs from 'fs';
2-
31
import { Logger, Module } from '@nestjs/common';
42
import { ConfigModule, ConfigService } from '@nestjs/config';
53
import { MongooseModule, MongooseModuleFactoryOptions } from '@nestjs/mongoose';
64

5+
import { validate } from 'class-validator';
76
import { AuthModule } from './auth/auth.module';
87
import { FileModule } from './file/file.module';
98
import { ParseTokenPipe } from './parseToken';
10-
import { SongModule } from './song/song.module';
119
import { SongBrowserModule } from './song-browser/song-browser.module';
10+
import { SongModule } from './song/song.module';
1211
import { UserModule } from './user/user.module';
1312

1413
@Module({
1514
imports: [
1615
ConfigModule.forRoot({
1716
isGlobal: true,
1817
envFilePath: ['.env.development', '.env.production'],
18+
validate: validate,
1919
}),
2020
//DatabaseModule,
2121
MongooseModule.forRootAsync({
@@ -44,46 +44,4 @@ import { UserModule } from './user/user.module';
4444
providers: [ParseTokenPipe],
4545
exports: [ParseTokenPipe],
4646
})
47-
export class AppModule {
48-
private readonly logger = new Logger(AppModule.name);
49-
constructor(private readonly configService: ConfigService) {
50-
// read .env.development.example file
51-
const file = '.env.development.example';
52-
const encoding = 'utf8';
53-
const fileData = fs.readFileSync(file, encoding);
54-
55-
const variableToIgnore = ['APP_DOMAIN', 'NODE_ENV', 'WHITELISTED_USERS'];
56-
57-
const variables = fileData
58-
.split('\n')
59-
// trim whitespace
60-
.map((line) => line.trim())
61-
// remove empty lines
62-
.filter((line) => line.length > 0)
63-
// get variable names
64-
.map((line) => line.split('=')[0])
65-
// remove variables that are not in the .env.development.example file
66-
.filter((variable) => !variableToIgnore.includes(variable));
67-
68-
this.logger.warn(`Ignoring variables: ${variableToIgnore.join(', ')}`);
69-
this.logger.warn(`Checking variables: ${variables.join(', ')}`);
70-
71-
let isMissing = false;
72-
73-
for (const variable of variables) {
74-
const value = this.configService.get(variable);
75-
76-
if (!value) {
77-
this.logger.error(
78-
`Missing environment variable ${variable} in env vars}`,
79-
);
80-
81-
isMissing = true;
82-
}
83-
}
84-
85-
if (isMissing) {
86-
throw new Error('Missing environment variables in env vars file');
87-
}
88-
}
89-
}
47+
export class AppModule {}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { plainToInstance } from 'class-transformer';
2+
import { IsEnum, IsOptional, IsString, validateSync } from 'class-validator';
3+
4+
enum Environment {
5+
Development = 'development',
6+
Production = 'production',
7+
}
8+
9+
export class EnvironmentVariables {
10+
@IsEnum(Environment)
11+
@IsOptional()
12+
NODE_ENV?: Environment;
13+
14+
@IsString()
15+
GITHUB_CLIENT_ID: string;
16+
17+
@IsString()
18+
GITHUB_CLIENT_SECRET: string;
19+
20+
@IsString()
21+
GOOGLE_CLIENT_ID: string;
22+
23+
@IsString()
24+
GOOGLE_CLIENT_SECRET: string;
25+
26+
@IsString()
27+
DISCORD_CLIENT_ID: string;
28+
29+
@IsString()
30+
DISCORD_CLIENT_SECRET: string;
31+
32+
@IsString()
33+
JWT_SECRET: string;
34+
35+
@IsString()
36+
JWT_EXPIRES_IN: string;
37+
38+
@IsString()
39+
JWT_REFRESH_SECRET: string;
40+
41+
@IsString()
42+
JWT_REFRESH_EXPIRES_IN: string;
43+
44+
@IsString()
45+
MONGO_URL: string;
46+
47+
@IsString()
48+
SERVER_URL: string;
49+
50+
@IsString()
51+
FRONTEND_URL: string;
52+
53+
@IsString()
54+
@IsOptional()
55+
APP_DOMAIN?: string;
56+
57+
@IsString()
58+
RECAPTCHA_KEY: string;
59+
60+
@IsString()
61+
S3_ENDPOINT: string;
62+
63+
@IsString()
64+
S3_BUCKET_SONGS: string;
65+
66+
@IsString()
67+
S3_BUCKET_THUMBS: string;
68+
69+
@IsString()
70+
S3_KEY: string;
71+
72+
@IsString()
73+
S3_SECRET: string;
74+
75+
@IsString()
76+
S3_REGION: string;
77+
78+
@IsString()
79+
@IsOptional()
80+
WHITELISTED_USERS?: string;
81+
82+
@IsString()
83+
DISCORD_WEBHOOK_URL: string;
84+
}
85+
86+
export function validate(config: Record<string, unknown>) {
87+
const validatedConfig = plainToInstance(EnvironmentVariables, config, {
88+
enableImplicitConversion: true,
89+
});
90+
91+
const errors = validateSync(validatedConfig, {
92+
skipMissingProperties: false,
93+
});
94+
95+
if (errors.length > 0) {
96+
throw new Error(errors.toString());
97+
}
98+
99+
return validatedConfig;
100+
}

0 commit comments

Comments
 (0)