Skip to content

Commit 548ff89

Browse files
authored
Merge pull request #30 from topcoder-platform/env-config
Env configuration
2 parents 6290075 + 74d8ef1 commit 548ff89

File tree

12 files changed

+155
-29
lines changed

12 files changed

+155
-29
lines changed

eslint.config.mjs

+7
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ export default tseslint.config(
3333
'@typescript-eslint/no-unsafe-assignment': 'off',
3434
'@typescript-eslint/no-unsafe-call': 'off',
3535
'@typescript-eslint/no-unsafe-member-access': 'off',
36+
'no-restricted-syntax': [
37+
'error',
38+
{
39+
selector: "MemberExpression[object.name='process'][property.name='env']",
40+
message: 'Use ENV_CONFIG instead of process.env',
41+
},
42+
],
3643
},
3744
},
3845
);

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"cors": "^2.8.5",
3232
"csv": "^6.3.11",
3333
"csv-stringify": "^6.5.2",
34+
"dotenv": "^16.5.0",
3435
"jsonwebtoken": "^9.0.2",
3536
"lodash": "^4.17.21",
3637
"reflect-metadata": "^0.2.2",

pnpm-lock.yaml

+21-12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/api/webhooks/trolley/trolley.service.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ import crypto from 'crypto';
22
import { Inject, Injectable } from '@nestjs/common';
33
import { trolley_webhook_log, webhook_status } from '@prisma/client';
44
import { PrismaService } from 'src/shared/global/prisma.service';
5+
import { ENV_CONFIG } from 'src/config';
56

67
enum TrolleyHeaders {
78
id = 'x-paymentrails-delivery',
89
signature = 'x-paymentrails-signature',
910
created = 'x-paymentrails-created',
1011
}
1112

12-
const trolleyWhHmac = process.env.TROLLEY_WH_HMAC;
13+
const trolleyWhHmac = ENV_CONFIG.TROLLEY_WH_HMAC;
1314
if (!trolleyWhHmac) {
1415
throw new Error('TROLLEY_WH_HMAC is not set!');
1516
}

src/config/config.env.ts

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { IsInt, IsOptional, IsString } from 'class-validator';
2+
3+
export class ConfigEnv {
4+
@IsString()
5+
@IsOptional()
6+
API_BASE = '/v5/finance';
7+
8+
@IsInt()
9+
@IsOptional()
10+
PORT = 3000;
11+
12+
@IsString()
13+
TOPCODER_API_BASE_URL!: string;
14+
15+
@IsString()
16+
AUTH0_M2M_AUDIENCE!: string;
17+
18+
@IsString()
19+
AUTH0_TC_PROXY_URL!: string;
20+
21+
@IsString()
22+
AUTH0_M2M_CLIENT_ID!: string;
23+
24+
@IsString()
25+
AUTH0_M2M_SECRET!: string;
26+
27+
@IsString()
28+
AUTH0_M2M_TOKEN_URL!: string;
29+
30+
@IsString()
31+
AUTH0_M2M_GRANT_TYPE!: string;
32+
33+
@IsString()
34+
AUTH0_CERT!: string;
35+
36+
@IsString()
37+
AUTH0_CLIENT_ID!: string;
38+
39+
@IsString()
40+
DATABASE_URL!: string;
41+
42+
@IsString()
43+
TROLLEY_WIDGET_BASE_URL!: string;
44+
45+
@IsString()
46+
TROLLEY_WH_HMAC!: string;
47+
48+
@IsString()
49+
TROLLEY_ACCESS_KEY!: string;
50+
51+
@IsString()
52+
TROLLEY_SECRET_KEY!: string;
53+
}

src/config/config.loader.ts

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import * as dotenv from 'dotenv';
2+
import { plainToInstance } from 'class-transformer';
3+
import { validateSync } from 'class-validator';
4+
import { Logger } from '@nestjs/common';
5+
import { ConfigEnv } from './config.env';
6+
7+
/**
8+
* Loads and validates environment variables into a `ConfigEnv` instance.
9+
*
10+
* This function uses `plainToInstance` to map environment variables from `process.env`
11+
* into a `ConfigEnv` class instance, enabling implicit conversion and exposing default values.
12+
* It then validates the resulting instance using `validateSync` to ensure all required
13+
* properties are present and conform to the expected constraints.
14+
*
15+
* If validation errors are found, they are logged using a `Logger` instance, and an error
16+
* is thrown to indicate invalid environment variables.
17+
*
18+
* @throws {Error} If any environment variables are invalid or missing.
19+
* @returns {ConfigEnv} A validated instance of the `ConfigEnv` class.
20+
*/
21+
function loadAndValidateEnv(): ConfigEnv {
22+
// eslint-disable-next-line no-restricted-syntax
23+
const env = plainToInstance(ConfigEnv, process.env, {
24+
enableImplicitConversion: true,
25+
exposeDefaultValues: true,
26+
});
27+
28+
const errors = validateSync(env, {
29+
skipMissingProperties: false,
30+
whitelist: true,
31+
});
32+
33+
if (errors.length > 0) {
34+
const logger = new Logger('Config');
35+
for (const err of errors) {
36+
logger.error(JSON.stringify(err.constraints));
37+
}
38+
throw new Error('Invalid environment variables');
39+
}
40+
41+
return env;
42+
}
43+
44+
dotenv.config();
45+
export const ENV_CONFIG = loadAndValidateEnv();

src/config/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './config.loader';

src/core/auth/middleware/tokenValidator.middleware.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
UnauthorizedException,
55
} from '@nestjs/common';
66
import * as jwt from 'jsonwebtoken';
7+
import { ENV_CONFIG } from 'src/config';
78

89
@Injectable()
910
export class TokenValidatorMiddleware implements NestMiddleware {
@@ -16,7 +17,7 @@ export class TokenValidatorMiddleware implements NestMiddleware {
1617

1718
let decoded: any;
1819
try {
19-
decoded = jwt.verify(idToken, process.env.AUTH0_CERT);
20+
decoded = jwt.verify(idToken, ENV_CONFIG.AUTH0_CERT);
2021
} catch (error) {
2122
console.error('Error verifying JWT', error);
2223
throw new UnauthorizedException('Invalid or expired JWT!');
@@ -29,8 +30,8 @@ export class TokenValidatorMiddleware implements NestMiddleware {
2930

3031
req.isM2M = !!decoded.scope;
3132
const aud = req.isM2M
32-
? process.env.AUTH0_M2M_AUDIENCE
33-
: process.env.AUTH0_CLIENT_ID;
33+
? ENV_CONFIG.AUTH0_M2M_AUDIENCE
34+
: ENV_CONFIG.AUTH0_CLIENT_ID;
3435

3536
if (decoded.aud !== aud) {
3637
req.idTokenVerified = false;

src/main.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ import { ApiModule } from './api/api.module';
77
import { AppModule } from './app.module';
88
import { PaymentProvidersModule } from './api/payment-providers/payment-providers.module';
99
import { WebhooksModule } from './api/webhooks/webhooks.module';
10+
import { ENV_CONFIG } from './config';
1011

1112
async function bootstrap() {
1213
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
1314
rawBody: true,
1415
});
1516

16-
// Global prefix for all routes is configured as `/v5/finance`
17-
app.setGlobalPrefix(process.env.API_BASE ?? '/v5/finance');
17+
// Global prefix for all routes
18+
app.setGlobalPrefix(ENV_CONFIG.API_BASE);
1819

1920
// CORS related settings
2021
const corsConfig: cors.CorsOptions = {
@@ -67,7 +68,7 @@ async function bootstrap() {
6768
);
6869
});
6970

70-
await app.listen(process.env.PORT ?? 3000);
71+
await app.listen(ENV_CONFIG.PORT ?? 3000);
7172
}
7273

7374
void bootstrap();

src/shared/global/trolley.service.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ import url from 'url';
22
import crypto from 'crypto';
33
import trolley from 'trolleyhq';
44
import { Injectable } from '@nestjs/common';
5+
import { ENV_CONFIG } from 'src/config';
56

6-
const { TROLLEY_ACCESS_KEY, TROLLEY_SECRET_KEY, TROLLEY_WIDGET_BASE_URL } =
7-
process.env;
7+
8+
const TROLLEY_ACCESS_KEY = ENV_CONFIG.TROLLEY_ACCESS_KEY;
9+
const TROLLEY_SECRET_KEY = ENV_CONFIG.TROLLEY_SECRET_KEY;
10+
const TROLLEY_WIDGET_BASE_URL = ENV_CONFIG.TROLLEY_WIDGET_BASE_URL;
811

912
const client = trolley({
1013
key: TROLLEY_ACCESS_KEY as string,

src/shared/topcoder/members.service.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import { chunk } from 'lodash';
22
import { Injectable } from '@nestjs/common';
33
import { MEMBER_FIELDS } from './member.types';
44
import { TopcoderM2MService } from './topcoder-m2m.service';
5+
import { ENV_CONFIG } from 'src/config';
6+
7+
const { TOPCODER_API_BASE_URL } = ENV_CONFIG;
58

69
@Injectable()
710
export class TopcoderMembersService {
@@ -21,7 +24,7 @@ export class TopcoderMembersService {
2124

2225
// Split the unique user IDs into chunks of 100 to comply with API request limits
2326
const requests = chunk(uniqUserIds, 30).map((chunk) => {
24-
const requestUrl = `${process.env.TOPCODER_API_BASE_URL}/members?${chunk.map((id) => `userIds[]=${id}`).join('&')}&fields=handle,userId`;
27+
const requestUrl = `${TOPCODER_API_BASE_URL}/members?${chunk.map((id) => `userIds[]=${id}`).join('&')}&fields=handle,userId`;
2528
return fetch(requestUrl).then(
2629
async (response) =>
2730
(await response.json()) as { handle: string; userId: string },
@@ -67,7 +70,7 @@ export class TopcoderMembersService {
6770
e.message ?? e,
6871
);
6972
}
70-
const requestUrl = `${process.env.TOPCODER_API_BASE_URL}/members/${handle}${fields ? `?fields=${fields.join(',')}` : ''}`;
73+
const requestUrl = `${TOPCODER_API_BASE_URL}/members/${handle}${fields ? `?fields=${fields.join(',')}` : ''}`;
7174

7275
try {
7376
const response: { [key: string]: string } = await fetch(requestUrl, {

src/shared/topcoder/topcoder-m2m.service.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Injectable } from '@nestjs/common';
2+
import { ENV_CONFIG } from 'src/config';
23

34
@Injectable()
45
export class TopcoderM2MService {
@@ -18,17 +19,17 @@ export class TopcoderM2MService {
1819
* - `AUTH0_M2M_GRANT_TYPE`: The grant type for the M2M token request.
1920
*/
2021
async getToken(): Promise<string | undefined> {
21-
const tokenURL = `${process.env.AUTH0_TC_PROXY_URL}/token`;
22+
const tokenURL = `${ENV_CONFIG.AUTH0_TC_PROXY_URL}/token`;
2223
try {
2324
const response = await fetch(tokenURL, {
2425
method: 'POST',
2526
headers: { 'Content-Type': 'application/json' },
2627
body: JSON.stringify({
27-
auth0_url: `${process.env.AUTH0_M2M_TOKEN_URL}/oauth/token`,
28-
client_id: process.env.AUTH0_M2M_CLIENT_ID,
29-
client_secret: process.env.AUTH0_M2M_SECRET,
30-
audience: process.env.AUTH0_M2M_AUDIENCE,
31-
grant_type: process.env.AUTH0_M2M_GRANT_TYPE,
28+
auth0_url: `${ENV_CONFIG.AUTH0_M2M_TOKEN_URL}/oauth/token`,
29+
client_id: ENV_CONFIG.AUTH0_M2M_CLIENT_ID,
30+
client_secret: ENV_CONFIG.AUTH0_M2M_SECRET,
31+
audience: ENV_CONFIG.AUTH0_M2M_AUDIENCE,
32+
grant_type: ENV_CONFIG.AUTH0_M2M_GRANT_TYPE,
3233
}),
3334
});
3435

0 commit comments

Comments
 (0)