From 8cc7383d62274971ad17bcb6f54c2bfa38725887 Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Tue, 26 May 2026 08:19:33 +0000 Subject: [PATCH] refactor: migrate environment variable access to appConfig for improved configuration management - Introduced appConfig to centralize configuration management and replace direct process.env access. - Updated various services and utilities to utilize appConfig for retrieving configuration values. - Removed deprecated helper functions for environment variable access. - Enhanced test environment handling by utilizing isTest() method from appConfig. - Ensured all database connection configurations are now sourced from appConfig. - Improved readability and maintainability of the codebase by consolidating configuration logic. --- backend/src/app.module.ts | 2 + .../authorization/auth-with-api.middleware.ts | 3 +- backend/src/authorization/auth.middleware.ts | 6 +- .../authorization/basic-auth.middleware.ts | 5 +- .../non-scoped-auth.middleware.ts | 6 +- .../src/authorization/saas-auth.middleware.ts | 3 +- .../temporary-auth.middleware.ts | 6 +- .../custom-agent-repository-extension.ts | 5 +- .../ai/user-ai-requests-v2.controller.ts | 5 +- .../entities/amplitude/amplitude.service.ts | 9 +- .../company-info-helper.service.ts | 3 +- .../invite-user-in-company.use.case.ts | 2 +- .../entities/connection/connection.entity.ts | 3 +- .../connection/utils/is-host-allowed.ts | 5 +- .../utils/validate-create-connection-data.ts | 5 +- .../email-config/email-config.service.ts | 20 +- .../src/entities/email/email/email.service.ts | 7 +- .../src/entities/logging/winston-logger.ts | 3 +- ...-rules-with-actions-and-events-body.dto.ts | 3 +- .../use-cases/create-action-rule.use.case.ts | 8 +- ...n-rule-with-actions-and-events.use.case.ts | 8 +- .../table-schema/utils/dynamodb-schema-op.ts | 3 +- .../find-tables-in-connection-v2.use.case.ts | 23 +- .../find-tables-in-connection.use.case.ts | 8 +- .../entities/user/utils/generate-gwt-token.ts | 5 +- .../user/utils/get-cookie-domain-options.ts | 4 +- .../helpers/app/get-requeired-env-variable.ts | 8 - backend/src/helpers/app/is-saas.ts | 5 +- backend/src/helpers/app/is-test.ts | 4 +- backend/src/helpers/constants/constants.ts | 83 +++-- backend/src/helpers/encryption/encryptor.ts | 5 +- backend/src/helpers/get-process-variable.ts | 4 - .../src/helpers/slack/slack-post-message.ts | 5 +- .../validators/is-action-url-host-allowed.ts | 3 +- ...equired-environment-variables.validator.ts | 30 -- .../helpers/validators/validation-helper.ts | 3 +- .../src/interceptors/timeout.interceptor.ts | 3 +- backend/src/main.ts | 8 +- .../base-saas-gateway.service.ts | 3 +- .../utils/generate-saas-jwt.ts | 7 +- backend/src/shared/config/app-config.ts | 345 ++++++++++++++++++ backend/src/shared/config/config.module.ts | 19 + backend/src/shared/config/config.service.ts | 154 ++------ .../src/shared/database/database.service.ts | 3 +- .../src/shared/services/turnstile.service.ts | 3 +- 45 files changed, 560 insertions(+), 295 deletions(-) delete mode 100644 backend/src/helpers/app/get-requeired-env-variable.ts delete mode 100644 backend/src/helpers/get-process-variable.ts delete mode 100644 backend/src/helpers/validators/required-environment-variables.validator.ts create mode 100644 backend/src/shared/config/app-config.ts create mode 100644 backend/src/shared/config/config.module.ts diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index bd5360d0a..309849ac0 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -46,12 +46,14 @@ import { SaaSGatewayModule } from './microservices/gateways/saas-gateway.ts/saas import { SaasModule } from './microservices/saas-microservice/saas.module.js'; import { AppLoggerMiddleware } from './middlewares/logging-middleware/app-logger-middlewate.js'; import { SelfHostedOperationsModule } from './selfhosted-operations/selhosted-operations.module.js'; +import { ConfigModule } from './shared/config/config.module.js'; import { DatabaseModule } from './shared/database/database.module.js'; import { SharedModule } from './shared/shared.module.js'; import { GetHelloUseCase } from './use-cases-app/get-hello.use.case.js'; @Module({ imports: [ + ConfigModule, ScheduleModule.forRoot(), ThrottlerModule.forRoot({ throttlers: [ diff --git a/backend/src/authorization/auth-with-api.middleware.ts b/backend/src/authorization/auth-with-api.middleware.ts index 404e63ff5..954a797d3 100644 --- a/backend/src/authorization/auth-with-api.middleware.ts +++ b/backend/src/authorization/auth-with-api.middleware.ts @@ -19,6 +19,7 @@ import { Messages } from '../exceptions/text/messages.js'; import { Constants } from '../helpers/constants/constants.js'; import { Encryptor } from '../helpers/encryption/encryptor.js'; import { isObjectEmpty } from '../helpers/is-object-empty.js'; +import { appConfig } from '../shared/config/app-config.js'; import { IRequestWithCognitoInfo } from './cognito-decoded.interface.js'; @Injectable() @@ -60,7 +61,7 @@ export class AuthWithApiMiddleware implements NestMiddleware { private async authenticateWithToken(tokenFromCookie: string, req: IRequestWithCognitoInfo): Promise { try { - const jwtSecret = process.env.JWT_SECRET; + const jwtSecret = appConfig.auth.jwtSecret; const data = jwt.verify(tokenFromCookie, jwtSecret) as jwt.JwtPayload; const userId = data.id; diff --git a/backend/src/authorization/auth.middleware.ts b/backend/src/authorization/auth.middleware.ts index 2738631f1..ef422e2ad 100644 --- a/backend/src/authorization/auth.middleware.ts +++ b/backend/src/authorization/auth.middleware.ts @@ -15,8 +15,10 @@ import { LogOutEntity } from '../entities/log-out/log-out.entity.js'; import { JwtScopesEnum } from '../entities/user/enums/jwt-scopes.enum.js'; import { UserEntity } from '../entities/user/user.entity.js'; import { Messages } from '../exceptions/text/messages.js'; +import { isTest } from '../helpers/app/is-test.js'; import { Constants } from '../helpers/constants/constants.js'; import { isObjectEmpty } from '../helpers/is-object-empty.js'; +import { appConfig } from '../shared/config/app-config.js'; import { IRequestWithCognitoInfo } from './cognito-decoded.interface.js'; @Injectable() @@ -32,7 +34,7 @@ export class AuthMiddleware implements NestMiddleware { try { token = req.cookies[Constants.JWT_COOKIE_KEY_NAME]; } catch (_e) { - if (process.env.NODE_ENV !== 'test') { + if (!isTest()) { throw new UnauthorizedException('JWT verification failed'); } } @@ -47,7 +49,7 @@ export class AuthMiddleware implements NestMiddleware { } try { - const jwtSecret = process.env.JWT_SECRET; + const jwtSecret = appConfig.auth.jwtSecret; const data = jwt.verify(token, jwtSecret) as jwt.JwtPayload; const userId = data.id; diff --git a/backend/src/authorization/basic-auth.middleware.ts b/backend/src/authorization/basic-auth.middleware.ts index 6512e2ffe..7e3297bb9 100644 --- a/backend/src/authorization/basic-auth.middleware.ts +++ b/backend/src/authorization/basic-auth.middleware.ts @@ -2,12 +2,13 @@ import { Injectable, NestMiddleware, UnauthorizedException } from '@nestjs/commo import auth from 'basic-auth'; import { Request, Response } from 'express'; import { Messages } from '../exceptions/text/messages.js'; +import { appConfig } from '../shared/config/app-config.js'; @Injectable() export class BasicAuthMiddleware implements NestMiddleware { use(req: Request, _res: Response, next: (err?: any, res?: any) => void): void { - const basicAuthLogin = process.env.BASIC_AUTH_LOGIN; - const basicAuthPassword = process.env.BASIC_AUTH_PWD; + const basicAuthLogin = appConfig.auth.basicAuthLogin; + const basicAuthPassword = appConfig.auth.basicAuthPassword; const userCredentials = auth(req); if (!userCredentials) { throw new UnauthorizedException(Messages.AUTHORIZATION_REQUIRED); diff --git a/backend/src/authorization/non-scoped-auth.middleware.ts b/backend/src/authorization/non-scoped-auth.middleware.ts index ed74bba3f..36177eb17 100644 --- a/backend/src/authorization/non-scoped-auth.middleware.ts +++ b/backend/src/authorization/non-scoped-auth.middleware.ts @@ -12,8 +12,10 @@ import jwt from 'jsonwebtoken'; import { Repository } from 'typeorm'; import { LogOutEntity } from '../entities/log-out/log-out.entity.js'; import { Messages } from '../exceptions/text/messages.js'; +import { isTest } from '../helpers/app/is-test.js'; import { Constants } from '../helpers/constants/constants.js'; import { isObjectEmpty } from '../helpers/is-object-empty.js'; +import { appConfig } from '../shared/config/app-config.js'; import { IRequestWithCognitoInfo } from './cognito-decoded.interface.js'; @Injectable() @@ -28,7 +30,7 @@ export class NonScopedAuthMiddleware implements NestMiddleware { try { token = req.cookies[Constants.JWT_COOKIE_KEY_NAME]; } catch (_e) { - if (process.env.NODE_ENV !== 'test') { + if (!isTest()) { throw new UnauthorizedException('JWT verification failed'); } } @@ -43,7 +45,7 @@ export class NonScopedAuthMiddleware implements NestMiddleware { } try { - const jwtSecret = process.env.JWT_SECRET; + const jwtSecret = appConfig.auth.jwtSecret; const data = jwt.verify(token, jwtSecret) as jwt.JwtPayload; const userId = data.id; if (!userId) { diff --git a/backend/src/authorization/saas-auth.middleware.ts b/backend/src/authorization/saas-auth.middleware.ts index 95002ed6a..c36de63b6 100644 --- a/backend/src/authorization/saas-auth.middleware.ts +++ b/backend/src/authorization/saas-auth.middleware.ts @@ -2,6 +2,7 @@ import { Injectable, NestMiddleware, UnauthorizedException } from '@nestjs/commo import { Response } from 'express'; import jwt from 'jsonwebtoken'; import { Messages } from '../exceptions/text/messages.js'; +import { appConfig } from '../shared/config/app-config.js'; import { IRequestWithCognitoInfo } from './cognito-decoded.interface.js'; import { extractTokenFromHeader } from './utils/extract-token-from-header.js'; @@ -14,7 +15,7 @@ export class SaaSAuthMiddleware implements NestMiddleware { throw new UnauthorizedException('Token is missing'); } try { - const jwtSecret = process.env.MICROSERVICE_JWT_SECRET; + const jwtSecret = appConfig.auth.microserviceJwtSecret; const data = jwt.verify(token, jwtSecret) as jwt.JwtPayload; const requestId = data.request_id; diff --git a/backend/src/authorization/temporary-auth.middleware.ts b/backend/src/authorization/temporary-auth.middleware.ts index 3658ef626..244d87b26 100644 --- a/backend/src/authorization/temporary-auth.middleware.ts +++ b/backend/src/authorization/temporary-auth.middleware.ts @@ -13,8 +13,10 @@ import { Repository } from 'typeorm'; import { LogOutEntity } from '../entities/log-out/log-out.entity.js'; import { UserEntity } from '../entities/user/user.entity.js'; import { Messages } from '../exceptions/text/messages.js'; +import { isTest } from '../helpers/app/is-test.js'; import { Constants } from '../helpers/constants/constants.js'; import { isObjectEmpty } from '../helpers/is-object-empty.js'; +import { appConfig } from '../shared/config/app-config.js'; import { IRequestWithCognitoInfo } from './cognito-decoded.interface.js'; @Injectable() @@ -30,7 +32,7 @@ export class TemporaryAuthMiddleware implements NestMiddleware { try { token = req.cookies[Constants.JWT_COOKIE_KEY_NAME]; } catch (_e) { - if (process.env.NODE_ENV !== 'test') { + if (!isTest()) { throw new UnauthorizedException('JWT verification failed'); } } @@ -45,7 +47,7 @@ export class TemporaryAuthMiddleware implements NestMiddleware { } try { - const jwtSecret = process.env.TEMPORARY_JWT_SECRET; + const jwtSecret = appConfig.auth.temporaryJwtSecret; const data = jwt.verify(token, jwtSecret) as jwt.JwtPayload; const userId = data.id; if (!userId) { diff --git a/backend/src/entities/agent/repository/custom-agent-repository-extension.ts b/backend/src/entities/agent/repository/custom-agent-repository-extension.ts index 6ce19698f..1d7388bce 100644 --- a/backend/src/entities/agent/repository/custom-agent-repository-extension.ts +++ b/backend/src/entities/agent/repository/custom-agent-repository-extension.ts @@ -1,5 +1,6 @@ import { ConnectionTypeTestEnum } from '@rocketadmin/shared-code/dist/src/shared/enums/connection-types-enum.js'; import { nanoid } from 'nanoid'; +import { isTest } from '../../../helpers/app/is-test.js'; import { ConnectionEntity } from '../../connection/connection.entity.js'; import { AgentEntity } from '../agent.entity.js'; import { IAgentRepository } from './agent.repository.interface.js'; @@ -12,7 +13,7 @@ export const customAgentRepositoryExtension: IAgentRepository = { async createNewAgentForConnection(connection: ConnectionEntity): Promise { const agent = new AgentEntity(); - const token = process.env.NODE_ENV !== 'test' ? nanoid(64) : this.getTestAgentToken(connection.type); + const token = !isTest() ? nanoid(64) : this.getTestAgentToken(connection.type); agent.setToken(token); agent.connection = connection; const savedAgent = await this.save(agent); @@ -41,7 +42,7 @@ export const customAgentRepositoryExtension: IAgentRepository = { }, getTestAgentToken(connectionType: ConnectionTypeTestEnum): string { - if (process.env.NODE_ENV !== 'test') throw new Error('Test agent token can only be used in test environment'); + if (!isTest()) throw new Error('Test agent token can only be used in test environment'); switch (connectionType) { case ConnectionTypeTestEnum.agent_oracledb: return 'ORACLE-TEST-AGENT-TOKEN'; diff --git a/backend/src/entities/ai/user-ai-requests-v2.controller.ts b/backend/src/entities/ai/user-ai-requests-v2.controller.ts index 60b9e7247..59b7954e3 100644 --- a/backend/src/entities/ai/user-ai-requests-v2.controller.ts +++ b/backend/src/entities/ai/user-ai-requests-v2.controller.ts @@ -21,6 +21,7 @@ import { UserId } from '../../decorators/user-id.decorator.js'; import { InTransactionEnum } from '../../enums/in-transaction.enum.js'; import { ConnectionEditGuard } from '../../guards/connection-edit.guard.js'; import { TableAiRequestGuard } from '../../guards/table-ai-request.guard.js'; +import { isTest } from '../../helpers/app/is-test.js'; import { ValidationHelper } from '../../helpers/validators/validation-helper.js'; import { SentryInterceptor } from '../../interceptors/sentry.interceptor.js'; import { IAISettingsAndWidgetsCreation, IRequestInfoFromTableV2 } from './ai-use-cases.interface.js'; @@ -51,7 +52,7 @@ export class UserAIRequestsControllerV2 { @ApiBody({ type: RequestInfoFromTableBodyDTO }) @ApiQuery({ name: 'tableName', required: true, type: String }) @ApiQuery({ name: 'threadId', required: false, type: String }) - @Timeout(process.env.NODE_ENV !== 'test' ? TimeoutDefaults.AI : TimeoutDefaults.AI_TEST) + @Timeout(!isTest() ? TimeoutDefaults.AI : TimeoutDefaults.AI_TEST) @Post('/ai/v4/request/:connectionId') public async requestInfoFromTableWithAIWithHistory( @SlugUuid('connectionId') connectionId: string, @@ -90,7 +91,7 @@ export class UserAIRequestsControllerV2 { description: 'AI settings and widgets creation job has been queued.', }) @UseGuards(ConnectionEditGuard) - @Timeout(process.env.NODE_ENV !== 'test' ? TimeoutDefaults.AI : TimeoutDefaults.AI_TEST) + @Timeout(!isTest() ? TimeoutDefaults.AI : TimeoutDefaults.AI_TEST) @Get('/ai/v2/setup/:connectionId') public async requestAISettingsAndWidgetsCreation( @SlugUuid('connectionId') connectionId: string, diff --git a/backend/src/entities/amplitude/amplitude.service.ts b/backend/src/entities/amplitude/amplitude.service.ts index 575c0b26f..ce92cfeec 100644 --- a/backend/src/entities/amplitude/amplitude.service.ts +++ b/backend/src/entities/amplitude/amplitude.service.ts @@ -3,6 +3,8 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { AmplitudeEventTypeEnum } from '../../enums/amplitude-event-type.enum.js'; +import { isTest } from '../../helpers/app/is-test.js'; +import { appConfig } from '../../shared/config/app-config.js'; import { UserEntity } from '../user/user.entity.js'; export interface AmplitudeLogOptions { @@ -23,8 +25,9 @@ export class AmplitudeService implements OnModuleInit { ) {} public onModuleInit(): void { - if (process.env.AMPLITUDE_API_KEY) { - this.client = Amplitude.init(process.env.AMPLITUDE_API_KEY); + const amplitudeApiKey = appConfig.thirdParty.amplitudeApiKey; + if (amplitudeApiKey) { + this.client = Amplitude.init(amplitudeApiKey); } } @@ -34,7 +37,7 @@ export class AmplitudeService implements OnModuleInit { options?: AmplitudeLogOptions, ): Promise { try { - if (process.env.NODE_ENV === 'test') return; + if (isTest()) return; let user_email = (await this.userRepository.findOne({ where: { id: user_id } }))?.email; if (!user_email && options) { user_email = options.user_email; diff --git a/backend/src/entities/company-info/company-info-helper.service.ts b/backend/src/entities/company-info/company-info-helper.service.ts index e1a4d9556..fa70390c7 100644 --- a/backend/src/entities/company-info/company-info-helper.service.ts +++ b/backend/src/entities/company-info/company-info-helper.service.ts @@ -3,6 +3,7 @@ import { IGlobalDatabaseContext } from '../../common/application/global-database import { BaseType } from '../../common/data-injection.tokens.js'; import { SubscriptionLevelEnum } from '../../enums/subscription-level.enum.js'; import { isSaaS } from '../../helpers/app/is-saas.js'; +import { isTest } from '../../helpers/app/is-test.js'; import { Constants } from '../../helpers/constants/constants.js'; import { SaasCompanyGatewayService } from '../../microservices/gateways/saas-gateway.ts/saas-company-gateway.service.js'; @@ -15,7 +16,7 @@ export class CompanyInfoHelperService { ) {} public async canInviteMoreUsers(companyId: string): Promise { - if (!isSaaS() || process.env.NODE_ENV === 'test') { + if (!isSaaS() || isTest()) { return true; } diff --git a/backend/src/entities/company-info/use-cases/invite-user-in-company.use.case.ts b/backend/src/entities/company-info/use-cases/invite-user-in-company.use.case.ts index 60253ee48..9566ee409 100644 --- a/backend/src/entities/company-info/use-cases/invite-user-in-company.use.case.ts +++ b/backend/src/entities/company-info/use-cases/invite-user-in-company.use.case.ts @@ -122,7 +122,7 @@ export class InviteUserInCompanyAndConnectionGroupUseCase email: invitedUserEmail, role: invitedUserCompanyRole, }; - if (process.env.NODE_ENV === 'test') { + if (isTest()) { invitationRO.verificationString = rawToken; } return invitationRO; diff --git a/backend/src/entities/connection/connection.entity.ts b/backend/src/entities/connection/connection.entity.ts index 5826c2485..ffec4d569 100644 --- a/backend/src/entities/connection/connection.entity.ts +++ b/backend/src/entities/connection/connection.entity.ts @@ -13,6 +13,7 @@ import { PrimaryColumn, Relation, } from 'typeorm'; +import { isTest } from '../../helpers/app/is-test.js'; import { Encryptor } from '../../helpers/encryption/encryptor.js'; import { isConnectionTypeAgent } from '../../helpers/is-connection-entity-agent.js'; import { AgentEntity } from '../agent/agent.entity.js'; @@ -154,7 +155,7 @@ export class ConnectionEntity { this.id = nanoid(8); } this.signing_key = Encryptor.generateRandomString(40); - if (process.env.NODE_ENV === 'test') { + if (isTest()) { this.signing_key = 'test'; } if (!isConnectionTypeAgent(this.type)) { diff --git a/backend/src/entities/connection/utils/is-host-allowed.ts b/backend/src/entities/connection/utils/is-host-allowed.ts index 811ad4668..607880128 100644 --- a/backend/src/entities/connection/utils/is-host-allowed.ts +++ b/backend/src/entities/connection/utils/is-host-allowed.ts @@ -4,6 +4,7 @@ import dns from 'dns'; import ipRangeCheck from 'ip-range-check'; import { Messages } from '../../../exceptions/text/messages.js'; import { isSaaS } from '../../../helpers/app/is-saas.js'; +import { isTest } from '../../../helpers/app/is-test.js'; import { Constants } from '../../../helpers/constants/constants.js'; import { isConnectionTypeAgent } from '../../../helpers/is-connection-entity-agent.js'; @@ -15,10 +16,10 @@ export interface HostCheckData { } export async function isHostAllowed(connectionData: HostCheckData): Promise { - if (isConnectionTypeAgent(connectionData.type) || process.env.NODE_ENV === 'test') { + if (isConnectionTypeAgent(connectionData.type) || isTest()) { return true; } - if (process.env.NODE_ENV !== 'test' && !isSaaS()) { + if (!isTest() && !isSaaS()) { return true; } diff --git a/backend/src/entities/connection/utils/validate-create-connection-data.ts b/backend/src/entities/connection/utils/validate-create-connection-data.ts index a0e9cc10e..bb4fc3944 100644 --- a/backend/src/entities/connection/utils/validate-create-connection-data.ts +++ b/backend/src/entities/connection/utils/validate-create-connection-data.ts @@ -7,6 +7,7 @@ import { } from '@rocketadmin/shared-code/dist/src/shared/enums/connection-types-enum.js'; import validator from 'validator'; import { Messages } from '../../../exceptions/text/messages.js'; +import { isTest } from '../../../helpers/app/is-test.js'; import { isConnectionTypeAgent } from '../../../helpers/is-connection-entity-agent.js'; import { toPrettyErrorsMsg } from '../../../helpers/to-pretty-errors-msg.js'; import { CreateConnectionDs } from '../application/data-structures/create-connection.ds.js'; @@ -43,7 +44,7 @@ export async function validateCreateConnectionData( ) { if (!database) errors.push(Messages.DATABASE_MISSING); - if (process.env.NODE_ENV !== 'test' && !ssh && host) { + if (!isTest() && !ssh && host) { if (!host.startsWith('mongodb+srv')) { if (!validator.isFQDN(host) && !validator.isIP(host)) { errors.push(Messages.HOST_NAME_INVALID); @@ -89,7 +90,7 @@ export async function validateCreateConnectionData( } function validateConnectionType(type: string): boolean { - if (process.env.NODE_ENV === 'test') { + if (isTest()) { return !!Object.keys(ConnectionTypeTestEnum).find((key) => key === type); } return !!Object.keys(ConnectionTypesEnum).find((key) => key === type); diff --git a/backend/src/entities/email/email-config/email-config.service.ts b/backend/src/entities/email/email-config/email-config.service.ts index 463554f5c..cd3440421 100644 --- a/backend/src/entities/email/email-config/email-config.service.ts +++ b/backend/src/entities/email/email-config/email-config.service.ts @@ -1,25 +1,21 @@ import { Injectable } from '@nestjs/common'; +import { appConfig } from '../../../shared/config/app-config.js'; import { IEmailConfig, IEmailConfigService } from './email-config.interface.js'; @Injectable() export class EmailConfigService implements IEmailConfigService { public getEmailServiceConfig(): IEmailConfig | string { - const pullConfig = process.env.EMAIL_CONFIG_STRING; - if (pullConfig) { - return pullConfig; + const { configString, host, port, username, password, nonSecure } = appConfig.email; + if (configString) { + return configString; } - const emailServiceHost = process.env.EMAIL_SERVICE_HOST; - const emailServicePort = parseInt(process.env.EMAIL_SERVICE_PORT, 10) || 25; - const emailServiceUserName = process.env.EMAIL_SERVICE_USERNAME; - const emailServicePassword = process.env.EMAIL_SERVICE_PASSWORD; - const nonSecure = !process.env.NON_SSL_EMAIL; return { - host: emailServiceHost, - port: emailServicePort, + host: host, + port: port, secure: nonSecure, auth: { - user: emailServiceUserName, - pass: emailServicePassword, + user: username, + pass: password, }, socketTimeout: 4 * 1000, connectionTimeout: 4 * 1000, diff --git a/backend/src/entities/email/email/email.service.ts b/backend/src/entities/email/email/email.service.ts index 97c988aac..f87233259 100644 --- a/backend/src/entities/email/email/email.service.ts +++ b/backend/src/entities/email/email/email.service.ts @@ -6,8 +6,9 @@ import * as nunjucks from 'nunjucks'; import PQueue from 'p-queue'; import { BaseType } from '../../../common/data-injection.tokens.js'; import { TableActionEventEnum } from '../../../enums/table-action-event-enum.js'; +import { isTest } from '../../../helpers/app/is-test.js'; import { Constants } from '../../../helpers/constants/constants.js'; -import { getProcessVariable } from '../../../helpers/get-process-variable.js'; +import { appConfig } from '../../../shared/config/app-config.js'; import { WinstonLogger } from '../../logging/winston-logger.js'; import { UserInfoMessageData } from '../../table-actions/table-actions-module/table-action-activation.service.js'; import { EmailLetter } from '../email-messages/email-message.js'; @@ -25,7 +26,7 @@ export interface ICronMessagingResults { @Injectable() export class EmailService { - private readonly emailFrom = getProcessVariable('EMAIL_FROM') || Constants.AUTOADMIN_SUPPORT_MAIL; + private readonly emailFrom = appConfig.email.from; constructor( @Inject(BaseType.NUNJUCKS) private readonly nunjucksEnv: nunjucks.Environment, @@ -34,7 +35,7 @@ export class EmailService { ) {} public async sendEmailToUser(letterContent: IMessage): Promise { - if (process.env.NODE_ENV === 'test') return; + if (isTest()) return; const mailResult = await this.sendEmailWithTimeout(letterContent); if (mailResult) { return mailResult; diff --git a/backend/src/entities/logging/winston-logger.ts b/backend/src/entities/logging/winston-logger.ts index f984cc528..5b3b90fbd 100644 --- a/backend/src/entities/logging/winston-logger.ts +++ b/backend/src/entities/logging/winston-logger.ts @@ -1,6 +1,7 @@ import { Injectable, LoggerService } from '@nestjs/common'; import winston from 'winston'; import { slackPostMessage } from '../../helpers/slack/slack-post-message.js'; +import { appConfig } from '../../shared/config/app-config.js'; import { LoggerTransports } from './logger-transports.config.js'; @Injectable() @@ -9,7 +10,7 @@ export class WinstonLogger implements LoggerService { constructor() { this.logger = winston.createLogger({ - level: process.env.LOG_LEVEL || 'info', + level: appConfig.app.logLevel, transports: LoggerTransports.default, }); } diff --git a/backend/src/entities/table-actions/table-action-rules-module/application/dto/create-action-rules-with-actions-and-events-body.dto.ts b/backend/src/entities/table-actions/table-action-rules-module/application/dto/create-action-rules-with-actions-and-events-body.dto.ts index 31ef5050e..d6b38c606 100644 --- a/backend/src/entities/table-actions/table-action-rules-module/application/dto/create-action-rules-with-actions-and-events-body.dto.ts +++ b/backend/src/entities/table-actions/table-action-rules-module/application/dto/create-action-rules-with-actions-and-events-body.dto.ts @@ -18,11 +18,12 @@ import { IsURLOptions } from 'validator'; import { TableActionEventEnum } from '../../../../../enums/table-action-event-enum.js'; import { TableActionMethodEnum } from '../../../../../enums/table-action-method-enum.js'; import { TableActionTypeEnum } from '../../../../../enums/table-action-type.enum.js'; +import { isTest } from '../../../../../helpers/app/is-test.js'; function IsUrlIfNotTest(validationOptions?: IsURLOptions) { return (object: NonNullable, propertyName: string) => { const decorators = [IsString()]; - if (process.env.NODE_ENV !== 'test') { + if (!isTest()) { decorators.push(IsUrl(validationOptions)); } applyDecorators(...decorators)(object, propertyName); diff --git a/backend/src/entities/table-actions/table-action-rules-module/use-cases/create-action-rule.use.case.ts b/backend/src/entities/table-actions/table-action-rules-module/use-cases/create-action-rule.use.case.ts index d7db5abe3..d62c361da 100644 --- a/backend/src/entities/table-actions/table-action-rules-module/use-cases/create-action-rule.use.case.ts +++ b/backend/src/entities/table-actions/table-action-rules-module/use-cases/create-action-rule.use.case.ts @@ -5,6 +5,7 @@ import { IGlobalDatabaseContext } from '../../../../common/application/global-da import { BaseType } from '../../../../common/data-injection.tokens.js'; import { TableActionMethodEnum } from '../../../../enums/table-action-method-enum.js'; import { Messages } from '../../../../exceptions/text/messages.js'; +import { isTest } from '../../../../helpers/app/is-test.js'; import { isActionUrlHostAllowed } from '../../../../helpers/validators/is-action-url-host-allowed.js'; import { validateStringWithEnum } from '../../../../helpers/validators/validate-string-with-enum.js'; import { ValidationHelper } from '../../../../helpers/validators/validation-helper.js'; @@ -115,7 +116,7 @@ export class CreateActionRuleUseCase if (!action.action_slack_url) { throw new BadRequestException(Messages.SLACK_URL_MISSING); } - if (process.env.NODE_ENV !== 'test' && !ValidationHelper.isValidUrl(action.action_slack_url)) { + if (!isTest() && !ValidationHelper.isValidUrl(action.action_slack_url)) { throw new BadRequestException(Messages.URL_INVALID); } const isSlackUrlAllowed = await isActionUrlHostAllowed(action.action_slack_url); @@ -127,10 +128,7 @@ export class CreateActionRuleUseCase throw new BadRequestException(Messages.INVALID_ACTION_METHOD(action.action_method)); } if (action.action_method === TableActionMethodEnum.URL) { - if ( - process.env.NODE_ENV !== 'test' && - (!action.action_url || !ValidationHelper.isValidUrl(action.action_url)) - ) { + if (!isTest() && (!action.action_url || !ValidationHelper.isValidUrl(action.action_url))) { throw new BadRequestException(Messages.URL_INVALID); } const isUrlAllowed = await isActionUrlHostAllowed(action.action_url); diff --git a/backend/src/entities/table-actions/table-action-rules-module/use-cases/update-action-rule-with-actions-and-events.use.case.ts b/backend/src/entities/table-actions/table-action-rules-module/use-cases/update-action-rule-with-actions-and-events.use.case.ts index 1b7b29918..57a0da52d 100644 --- a/backend/src/entities/table-actions/table-action-rules-module/use-cases/update-action-rule-with-actions-and-events.use.case.ts +++ b/backend/src/entities/table-actions/table-action-rules-module/use-cases/update-action-rule-with-actions-and-events.use.case.ts @@ -5,6 +5,7 @@ import { IGlobalDatabaseContext } from '../../../../common/application/global-da import { BaseType } from '../../../../common/data-injection.tokens.js'; import { TableActionMethodEnum } from '../../../../enums/table-action-method-enum.js'; import { Messages } from '../../../../exceptions/text/messages.js'; +import { isTest } from '../../../../helpers/app/is-test.js'; import { isActionUrlHostAllowed } from '../../../../helpers/validators/is-action-url-host-allowed.js'; import { validateStringWithEnum } from '../../../../helpers/validators/validate-string-with-enum.js'; import { ValidationHelper } from '../../../../helpers/validators/validation-helper.js'; @@ -179,7 +180,7 @@ export class UpdateRuleUseCase if (!action.action_slack_url) { throw new BadRequestException(Messages.SLACK_URL_MISSING); } - if (process.env.NODE_ENV !== 'test' && !ValidationHelper.isValidUrl(action.action_slack_url)) { + if (!isTest() && !ValidationHelper.isValidUrl(action.action_slack_url)) { throw new BadRequestException(Messages.URL_INVALID); } const isSlackUrlAllowed = await isActionUrlHostAllowed(action.action_slack_url); @@ -193,10 +194,7 @@ export class UpdateRuleUseCase } if (action.action_method === TableActionMethodEnum.URL) { - if ( - process.env.NODE_ENV !== 'test' && - (!action.action_url || !ValidationHelper.isValidUrl(action.action_url)) - ) { + if (!isTest() && (!action.action_url || !ValidationHelper.isValidUrl(action.action_url))) { throw new BadRequestException(Messages.URL_INVALID); } const isUrlAllowed = await isActionUrlHostAllowed(action.action_url); diff --git a/backend/src/entities/table-schema/utils/dynamodb-schema-op.ts b/backend/src/entities/table-schema/utils/dynamodb-schema-op.ts index 0930ae30c..55b729f8e 100644 --- a/backend/src/entities/table-schema/utils/dynamodb-schema-op.ts +++ b/backend/src/entities/table-schema/utils/dynamodb-schema-op.ts @@ -22,6 +22,7 @@ import { UpdateTimeToLiveCommand, } from '@aws-sdk/client-dynamodb'; import { BadRequestException } from '@nestjs/common'; +import { isTest } from '../../../helpers/app/is-test.js'; import { SchemaChangeTypeEnum } from '../table-schema-change-enums.js'; export interface DynamoDbExecutionConnection { @@ -211,7 +212,7 @@ function buildDynamoDbClient(connection: DynamoDbExecutionConnection): DynamoDB const region = regionMatch ? regionMatch[1] : 'us-east-1'; return new DynamoDB({ endpoint, - region: process.env.NODE_ENV === 'test' ? 'localhost' : region, + region: isTest() ? 'localhost' : region, credentials: { accessKeyId: connection.username ?? '', secretAccessKey: connection.password ?? '', diff --git a/backend/src/entities/table/use-cases/find-tables-in-connection-v2.use.case.ts b/backend/src/entities/table/use-cases/find-tables-in-connection-v2.use.case.ts index 27bf6888a..65cc04df2 100644 --- a/backend/src/entities/table/use-cases/find-tables-in-connection-v2.use.case.ts +++ b/backend/src/entities/table/use-cases/find-tables-in-connection-v2.use.case.ts @@ -9,17 +9,18 @@ import { AmplitudeEventTypeEnum } from '../../../enums/amplitude-event-type.enum import { ExceptionOperations } from '../../../exceptions/custom-exceptions/exception-operation.js'; import { UnknownSQLException } from '../../../exceptions/custom-exceptions/unknown-sql-exception.js'; import { Messages } from '../../../exceptions/text/messages.js'; +import { isTest as isTestEnv } from '../../../helpers/app/is-test.js'; import { isConnectionTypeAgent } from '../../../helpers/is-connection-entity-agent.js'; import { AmplitudeService } from '../../amplitude/amplitude.service.js'; +import { CedarPermissionsService } from '../../cedar-authorization/cedar-permissions.service.js'; import { ConnectionEntity } from '../../connection/connection.entity.js'; import { isTestConnectionUtil } from '../../connection/utils/is-test-connection-util.js'; import { WinstonLogger } from '../../logging/winston-logger.js'; import { ITableAndViewPermissionData } from '../../permission/permission.interface.js'; import { FindTablesDs } from '../application/data-structures/find-tables.ds.js'; import { FoundTableDs, FoundTablesWithCategoriesDS } from '../application/data-structures/found-table.ds.js'; -import { saveTableInfoInDatabase } from '../utils/save-table-info-in-database-orchestrator.util.js'; import { addDisplayNamesForTables } from '../utils/add-display-names-for-tables.util.js'; -import { CedarPermissionsService } from '../../cedar-authorization/cedar-permissions.service.js'; +import { saveTableInfoInDatabase } from '../utils/save-table-info-in-database-orchestrator.util.js'; import { IFindTablesInConnectionV2 } from './table-use-cases.interface.js'; @Injectable({ scope: Scope.REQUEST }) @@ -98,17 +99,16 @@ export class FindTablesInConnectionV2UseCase userId, { tablesCount: tables?.length ? tables.length : 0 }, ); - if ( - connection.saved_table_info === 0 && - !connection.isTestConnection && - operationResult && - process.env.NODE_ENV !== 'test' - ) { + if (connection.saved_table_info === 0 && !connection.isTestConnection && operationResult && !isTestEnv()) { saveTableInfoInDatabase(connection.id, tables, masterPwd, this._dbContext); } } const tableNames = tables.map((t) => t.tableName); - const permissionsArr = await this.cedarPermissions.getUserPermissionsForAvailableTables(userId, connectionId, tableNames); + const permissionsArr = await this.cedarPermissions.getUserPermissionsForAvailableTables( + userId, + connectionId, + tableNames, + ); const tablesWithPermissions: Array = permissionsArr.map((perm) => ({ ...perm, isView: tables.find((t) => t.tableName === perm.tableName)?.isView || false, @@ -123,10 +123,7 @@ export class FindTablesInConnectionV2UseCase return !foundConnectionProperties.hidden_tables.includes(tableRO.table); }); } else { - const userConnectionEdit = await this.cedarPermissions.checkUserConnectionEdit( - userId, - connectionId, - ); + const userConnectionEdit = await this.cedarPermissions.checkUserConnectionEdit(userId, connectionId); if (!userConnectionEdit) { throw new HttpException( { diff --git a/backend/src/entities/table/use-cases/find-tables-in-connection.use.case.ts b/backend/src/entities/table/use-cases/find-tables-in-connection.use.case.ts index e77cc3f92..6023a4bf5 100644 --- a/backend/src/entities/table/use-cases/find-tables-in-connection.use.case.ts +++ b/backend/src/entities/table/use-cases/find-tables-in-connection.use.case.ts @@ -10,6 +10,7 @@ import { AmplitudeEventTypeEnum } from '../../../enums/amplitude-event-type.enum import { ExceptionOperations } from '../../../exceptions/custom-exceptions/exception-operation.js'; import { UnknownSQLException } from '../../../exceptions/custom-exceptions/unknown-sql-exception.js'; import { Messages } from '../../../exceptions/text/messages.js'; +import { isTest as isTestEnv } from '../../../helpers/app/is-test.js'; import { isConnectionTypeAgent } from '../../../helpers/is-connection-entity-agent.js'; import { AmplitudeService } from '../../amplitude/amplitude.service.js'; import { CedarPermissionsService } from '../../cedar-authorization/cedar-permissions.service.js'; @@ -102,12 +103,7 @@ export class FindTablesInConnectionUseCase userId, { tablesCount: tables?.length ? tables.length : 0 }, ); - if ( - connection.saved_table_info === 0 && - !connection.isTestConnection && - operationResult && - process.env.NODE_ENV !== 'test' - ) { + if (connection.saved_table_info === 0 && !connection.isTestConnection && operationResult && !isTestEnv()) { saveTableInfoInDatabase(connection.id, tables, masterPwd, this._dbContext); } } diff --git a/backend/src/entities/user/utils/generate-gwt-token.ts b/backend/src/entities/user/utils/generate-gwt-token.ts index c34ccca6c..b5e67f047 100644 --- a/backend/src/entities/user/utils/generate-gwt-token.ts +++ b/backend/src/entities/user/utils/generate-gwt-token.ts @@ -1,5 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import jwt from 'jsonwebtoken'; +import { appConfig } from '../../../shared/config/app-config.js'; import { JwtScopesEnum } from '../enums/jwt-scopes.enum.js'; import { UserEntity } from '../user.entity.js'; @@ -7,7 +8,7 @@ export function generateGwtToken(user: UserEntity, scope: Array): const today = new Date(); const exp = new Date(today); exp.setTime(today.getTime() + 60 * 60 * 1000 * 24 * 7); - const jwtSecret = process.env.JWT_SECRET; + const jwtSecret = appConfig.auth.jwtSecret; const token = jwt.sign( { id: user.id, @@ -28,7 +29,7 @@ export function generateTemporaryJwtToken(user: UserEntity): IToken { const today = new Date(); const exp = new Date(today); exp.setTime(today.getTime() + 1000 * 60 * 4); - const jwtSecret = process.env.TEMPORARY_JWT_SECRET; + const jwtSecret = appConfig.auth.temporaryJwtSecret; const token = jwt.sign( { id: user.id, diff --git a/backend/src/entities/user/utils/get-cookie-domain-options.ts b/backend/src/entities/user/utils/get-cookie-domain-options.ts index 05bb61876..78904368d 100644 --- a/backend/src/entities/user/utils/get-cookie-domain-options.ts +++ b/backend/src/entities/user/utils/get-cookie-domain-options.ts @@ -1,5 +1,7 @@ +import { appConfig } from '../../../shared/config/app-config.js'; + export function getCookieDomainOptions(requestHostname: string): { domain: string } | undefined { - const cookieDomain = process.env.ROCKETADMIN_COOKIE_DOMAIN; + const cookieDomain = appConfig.app.cookieDomain; if (cookieDomain && requestHostname?.includes(cookieDomain)) { return { domain: cookieDomain }; } diff --git a/backend/src/helpers/app/get-requeired-env-variable.ts b/backend/src/helpers/app/get-requeired-env-variable.ts deleted file mode 100644 index e27e6a7e6..000000000 --- a/backend/src/helpers/app/get-requeired-env-variable.ts +++ /dev/null @@ -1,8 +0,0 @@ -export function getRequiredEnvVariable(variableName: string): string { - // eslint-disable-next-line security/detect-object-injection - const variableValue = process.env[variableName]; - if (!variableValue) { - throw new Error(`Environment variable ${variableName} is not set`); - } - return variableValue; -} diff --git a/backend/src/helpers/app/is-saas.ts b/backend/src/helpers/app/is-saas.ts index b11eda795..3acd76f9a 100644 --- a/backend/src/helpers/app/is-saas.ts +++ b/backend/src/helpers/app/is-saas.ts @@ -1,6 +1,5 @@ -import { getProcessVariable } from '../get-process-variable.js'; +import { appConfig } from '../../shared/config/app-config.js'; export function isSaaS(): boolean { - const isSaaS: unknown = getProcessVariable('IS_SAAS'); - return !!isSaaS; + return appConfig.isSaaS; } diff --git a/backend/src/helpers/app/is-test.ts b/backend/src/helpers/app/is-test.ts index cb770d07d..a98dfcdb0 100644 --- a/backend/src/helpers/app/is-test.ts +++ b/backend/src/helpers/app/is-test.ts @@ -1,3 +1,5 @@ +import { appConfig } from '../../shared/config/app-config.js'; + export function isTest(): boolean { - return process.env.NODE_ENV === 'test'; + return appConfig.isTest; } diff --git a/backend/src/helpers/constants/constants.ts b/backend/src/helpers/constants/constants.ts index 4f290689e..9fc561cd6 100644 --- a/backend/src/helpers/constants/constants.ts +++ b/backend/src/helpers/constants/constants.ts @@ -1,9 +1,9 @@ import { ConnectionTypesEnum } from '@rocketadmin/shared-code/dist/src/shared/enums/connection-types-enum.js'; import { Knex } from 'knex'; import { CreateConnectionDto } from '../../entities/connection/application/dto/create-connection.dto.js'; +import { appConfig } from '../../shared/config/app-config.js'; import { isSaaS } from '../app/is-saas.js'; import { isTest } from '../app/is-test.js'; -import { getProcessVariable } from '../get-process-variable.js'; import { parseTestDynamoDBConnectionString, parseTestMongoDBConnectionString, @@ -142,11 +142,11 @@ export const Constants = { title: 'Postgres', masterEncryption: false, type: ConnectionTypesEnum.postgres, - username: getProcessVariable('POSTGRES_CONNECTION_USERNAME') || null, - password: getProcessVariable('POSTGRES_CONNECTION_PASSWORD') || null, - host: getProcessVariable('POSTGRES_CONNECTION_HOST') || null, - port: parseInt(getProcessVariable('POSTGRES_CONNECTION_PORT'), 10) || null, - database: getProcessVariable('POSTGRES_CONNECTION_DATABASE') || null, + username: appConfig.testDb.postgres.username, + password: appConfig.testDb.postgres.password, + host: appConfig.testDb.postgres.host, + port: appConfig.testDb.postgres.port, + database: appConfig.testDb.postgres.database, isTestConnection: true, }, @@ -154,11 +154,11 @@ export const Constants = { title: 'MSSQL', masterEncryption: false, type: ConnectionTypesEnum.mssql, - host: getProcessVariable('MSSQL_CONNECTION_HOST') || null, - port: parseInt(getProcessVariable('MSSQL_CONNECTION_PORT'), 10) || null, - password: getProcessVariable('MSSQL_CONNECTION_PASSWORD') || null, - username: getProcessVariable('MSSQL_CONNECTION_USERNAME') || null, - database: getProcessVariable('MSSQL_CONNECTION_DATABASE') || null, + host: appConfig.testDb.mssql.host, + port: appConfig.testDb.mssql.port, + password: appConfig.testDb.mssql.password, + username: appConfig.testDb.mssql.username, + database: appConfig.testDb.mssql.database, ssh: false, ssl: false, isTestConnection: true, @@ -167,52 +167,52 @@ export const Constants = { TEST_CONNECTION_TO_ORACLE: { title: 'Oracle', type: ConnectionTypesEnum.oracledb, - host: getProcessVariable('ORACLE_CONNECTION_HOST') || null, - port: parseInt(getProcessVariable('ORACLE_CONNECTION_PORT'), 10) || null, - username: getProcessVariable('ORACLE_CONNECTION_USERNAME') || null, - password: getProcessVariable('ORACLE_CONNECTION_PASSWORD') || null, - database: getProcessVariable('ORACLE_CONNECTION_DATABASE') || null, - sid: getProcessVariable('ORACLE_CONNECTION_SID') || null, + host: appConfig.testDb.oracle.host, + port: appConfig.testDb.oracle.port, + username: appConfig.testDb.oracle.username, + password: appConfig.testDb.oracle.password, + database: appConfig.testDb.oracle.database, + sid: appConfig.testDb.oracle.sid, isTestConnection: true, }, TEST_SSH_CONNECTION_TO_MYSQL: { title: 'MySQL', type: ConnectionTypesEnum.mysql, - host: getProcessVariable('MYSQL_CONNECTION_HOST') || null, - port: parseInt(getProcessVariable('MYSQL_CONNECTION_PORT'), 10) || null, - username: getProcessVariable('MYSQL_CONNECTION_USERNAME') || null, - password: getProcessVariable('MYSQL_CONNECTION_PASSWORD') || null, - database: getProcessVariable('MYSQL_CONNECTION_DATABASE') || null, + host: appConfig.testDb.mysql.host, + port: appConfig.testDb.mysql.port, + username: appConfig.testDb.mysql.username, + password: appConfig.testDb.mysql.password, + database: appConfig.testDb.mysql.database, ssh: true, isTestConnection: true, - sshHost: getProcessVariable('MYSQL_CONNECTION_SSH_HOST') || null, - sshPort: getProcessVariable('MYSQL_CONNECTION_SSH_PORT') || null, - sshUsername: getProcessVariable('MYSQL_CONNECTION_SSH_USERNAME') || null, - privateSSHKey: getProcessVariable('MYSQL_CONNECTION_SSH_KEY') || null, + sshHost: appConfig.testDb.mysql.sshHost, + sshPort: appConfig.testDb.mysql.sshPort, + sshUsername: appConfig.testDb.mysql.sshUsername, + privateSSHKey: appConfig.testDb.mysql.sshKey, }, TEST_CONNECTION_TO_MONGO: { title: 'MongoDB', type: ConnectionTypesEnum.mongodb, - host: getProcessVariable('MONGO_CONNECTION_HOST') || null, - port: parseInt(getProcessVariable('MONGO_CONNECTION_PORT'), 10) || null, - username: getProcessVariable('MONGO_CONNECTION_USERNAME') || null, - password: getProcessVariable('MONGO_CONNECTION_PASSWORD') || null, - database: getProcessVariable('MONGO_CONNECTION_DATABASE') || null, - authSource: getProcessVariable('MONGO_CONNECTION_AUTH_SOURCE') || null, + host: appConfig.testDb.mongo.host, + port: appConfig.testDb.mongo.port, + username: appConfig.testDb.mongo.username, + password: appConfig.testDb.mongo.password, + database: appConfig.testDb.mongo.database, + authSource: appConfig.testDb.mongo.authSource, isTestConnection: true, }, TEST_CONNECTION_TO_IBMBD2: { title: 'IBM DB2', type: ConnectionTypesEnum.ibmdb2, - host: getProcessVariable('IBM_DB2_CONNECTION_HOST') || null, - port: parseInt(getProcessVariable('IBM_DB2_CONNECTION_PORT'), 10) || null, - username: getProcessVariable('IBM_DB2_CONNECTION_USERNAME') || null, - password: getProcessVariable('IBM_DB2_CONNECTION_PASSWORD') || null, - database: getProcessVariable('IBM_DB2_CONNECTION_DATABASE') || null, - schema: getProcessVariable('IBM_DB2_CONNECTION_SCHEMA') || null, + host: appConfig.testDb.ibmdb2.host, + port: appConfig.testDb.ibmdb2.port, + username: appConfig.testDb.ibmdb2.username, + password: appConfig.testDb.ibmdb2.password, + database: appConfig.testDb.ibmdb2.database, + schema: appConfig.testDb.ibmdb2.schema, isTestConnection: true, }, @@ -221,8 +221,7 @@ export const Constants = { REMOVED_SENSITIVE_FIELD_IF_NOT_CHANGED: '', getTestConnectionsArr: (): Array => { - const isSaaS = process.env.IS_SAAS; - if (!isSaaS || isSaaS !== 'true') { + if (!isSaaS()) { return []; } @@ -249,7 +248,7 @@ export const Constants = { if (!isSaaS()) { return []; } - const testConnectionsJSON = getProcessVariable('TEST_CONNECTIONS'); + const testConnectionsJSON = appConfig.testDb.testConnectionsJson; if (!testConnectionsJSON) { return null; } @@ -296,7 +295,7 @@ export const Constants = { return this.getTestConnectionsArr().map((connection) => connection.host); }, - APP_DOMAIN_ADDRESS: process.env.APP_DOMAIN_ADDRESS || `http://127.0.0.1:3000`, + APP_DOMAIN_ADDRESS: appConfig.app.domainAddress, ALLOWED_REQUEST_DOMAIN: (): string => { if (isTest()) { return Constants.APP_DOMAIN_ADDRESS; diff --git a/backend/src/helpers/encryption/encryptor.ts b/backend/src/helpers/encryption/encryptor.ts index 03d3341e7..e767176f8 100644 --- a/backend/src/helpers/encryption/encryptor.ts +++ b/backend/src/helpers/encryption/encryptor.ts @@ -5,6 +5,7 @@ import crypto, { createHmac, randomBytes, scrypt } from 'crypto'; import CryptoJS from 'crypto-js'; import { ConnectionEntity } from '../../entities/connection/connection.entity.js'; import { EncryptionAlgorithmEnum } from '../../enums/encryption-algorithm.enum.js'; +import { appConfig } from '../../shared/config/app-config.js'; import { Constants } from '../constants/constants.js'; const ENCRYPTION_VERSION_PREFIX = '$v2:k1$'; @@ -16,7 +17,7 @@ const KEY_LENGTH = 32; export class Encryptor { static getPrivateKey(): string { - return process.env.PRIVATE_KEY; + return appConfig.auth.privateKey; } private static deriveKey(passphrase: string, salt: Buffer): Buffer { @@ -223,7 +224,7 @@ export class Encryptor { } static getUserIntercomHash(userId: string): string | null { - const intercomKey = process.env.INTERCOM_KEY; + const intercomKey = appConfig.thirdParty.intercomKey; if (!intercomKey) { return null; } diff --git a/backend/src/helpers/get-process-variable.ts b/backend/src/helpers/get-process-variable.ts deleted file mode 100644 index b8dc8c1ff..000000000 --- a/backend/src/helpers/get-process-variable.ts +++ /dev/null @@ -1,4 +0,0 @@ -export function getProcessVariable(variableName: string): string | null { - // eslint-disable-next-line security/detect-object-injection - return process.env[variableName] ? process.env[variableName] : null; -} diff --git a/backend/src/helpers/slack/slack-post-message.ts b/backend/src/helpers/slack/slack-post-message.ts index fa4a48906..533e14941 100644 --- a/backend/src/helpers/slack/slack-post-message.ts +++ b/backend/src/helpers/slack/slack-post-message.ts @@ -1,10 +1,11 @@ import axios from 'axios'; +import { appConfig } from '../../shared/config/app-config.js'; import { Constants } from '../constants/constants.js'; export async function slackPostMessage(message: string, channel = Constants.DEFAULT_SLACK_CHANNEL): Promise { try { - const slackBotToken = process.env.SLACK_BOT_ACCESS_TOKEN; - if (process.env.NODE_ENV === 'test' || !slackBotToken) { + const slackBotToken = appConfig.thirdParty.slackBotAccessToken; + if (appConfig.isTest || !slackBotToken) { return; } const url = 'https://slack.com/api/chat.postMessage'; diff --git a/backend/src/helpers/validators/is-action-url-host-allowed.ts b/backend/src/helpers/validators/is-action-url-host-allowed.ts index 2ef22ce08..658de04cd 100644 --- a/backend/src/helpers/validators/is-action-url-host-allowed.ts +++ b/backend/src/helpers/validators/is-action-url-host-allowed.ts @@ -1,10 +1,11 @@ import dns from 'dns'; import ipRangeCheck from 'ip-range-check'; import { isSaaS } from '../app/is-saas.js'; +import { isTest } from '../app/is-test.js'; import { Constants } from '../constants/constants.js'; export async function isActionUrlHostAllowed(url: string): Promise { - if (process.env.NODE_ENV === 'test') { + if (isTest()) { return true; } diff --git a/backend/src/helpers/validators/required-environment-variables.validator.ts b/backend/src/helpers/validators/required-environment-variables.validator.ts deleted file mode 100644 index c8fe67bea..000000000 --- a/backend/src/helpers/validators/required-environment-variables.validator.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Messages } from '../../exceptions/text/messages.js'; - -export function requiredEnvironmentVariablesValidator(): void { - const requiredParameterNames: Array = ['PRIVATE_KEY', 'JWT_SECRET']; - - const pgLiteFolderPath = getEnvironmentVariable('PGLITE_FOLDER_PATH'); - if (!pgLiteFolderPath) { - requiredParameterNames.push('DATABASE_URL'); - } - - const requiredParameters: Array<{ [k: string]: string | null }> = requiredParameterNames.map((paramName) => { - const paramValue = getEnvironmentVariable(paramName); - return { - [paramName]: paramValue, - }; - }); - const emptyRequiredParameterNames: Array = requiredParameters - .filter((param) => { - return !param[Object.keys(param)[0]]; - }) - .map((param) => Object.keys(param)[0]); - if (emptyRequiredParameterNames.length > 0) { - throw new Error(Messages.REQUIRED_PARAMETERS_MISSING(emptyRequiredParameterNames)); - } -} - -function getEnvironmentVariable(key: string): string | null { - // eslint-disable-next-line security/detect-object-injection - return process.env[key] || null; -} diff --git a/backend/src/helpers/validators/validation-helper.ts b/backend/src/helpers/validators/validation-helper.ts index 37469182d..b64bc3943 100644 --- a/backend/src/helpers/validators/validation-helper.ts +++ b/backend/src/helpers/validators/validation-helper.ts @@ -2,6 +2,7 @@ import { BadRequestException, HttpException, HttpStatus } from '@nestjs/common'; import countries from 'i18n-iso-countries'; import validator from 'validator'; import { Messages } from '../../exceptions/text/messages.js'; +import { isTest } from '../app/is-test.js'; import { Constants } from '../constants/constants.js'; export class ValidationHelper { @@ -81,7 +82,7 @@ export class ValidationHelper { } public static isPasswordStrongOrThrowError(password: string): boolean { - if (process.env.NODE_ENV === 'test') { + if (isTest()) { return true; } const result = validator.isStrongPassword(password, { diff --git a/backend/src/interceptors/timeout.interceptor.ts b/backend/src/interceptors/timeout.interceptor.ts index 89e95df44..2ddf65f2e 100644 --- a/backend/src/interceptors/timeout.interceptor.ts +++ b/backend/src/interceptors/timeout.interceptor.ts @@ -4,6 +4,7 @@ import { Observable, TimeoutError, throwError } from 'rxjs'; import { catchError, timeout } from 'rxjs/operators'; import { TIMEOUT_KEY, TimeoutDefaults } from '../decorators/timeout.decorator.js'; import { Messages } from '../exceptions/text/messages.js'; +import { isTest } from '../helpers/app/is-test.js'; @Injectable() export class TimeoutInterceptor implements NestInterceptor { @@ -15,7 +16,7 @@ export class TimeoutInterceptor implements NestInterceptor { context.getClass(), ]); - const defaultTimeout = process.env.NODE_ENV !== 'test' ? TimeoutDefaults.DEFAULT : TimeoutDefaults.DEFAULT_TEST; + const defaultTimeout = !isTest() ? TimeoutDefaults.DEFAULT : TimeoutDefaults.DEFAULT_TEST; const timeoutMs = customTimeout ?? defaultTimeout; diff --git a/backend/src/main.ts b/backend/src/main.ts index 72fc56809..417b8439a 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -12,11 +12,11 @@ import { WinstonLogger } from './entities/logging/winston-logger.js'; import { AllExceptionsFilter } from './exceptions/all-exceptions.filter.js'; import { ValidationException } from './exceptions/custom-exceptions/validation-exception.js'; import { Constants } from './helpers/constants/constants.js'; -import { requiredEnvironmentVariablesValidator } from './helpers/validators/required-environment-variables.validator.js'; +import { appConfig } from './shared/config/app-config.js'; async function bootstrap() { try { - requiredEnvironmentVariablesValidator(); + appConfig.validate(); const appOptions: NestApplicationOptions = { rawBody: true, logger: new WinstonLogger(), @@ -27,11 +27,11 @@ async function bootstrap() { app.set('query parser', 'extended'); Sentry.init({ - dsn: process.env.SENTRY_DSN, + dsn: appConfig.thirdParty.sentryDsn ?? undefined, tracesSampleRate: 1.0, }); - const globalPrefix = process.env.GLOBAL_PREFIX || '/'; + const globalPrefix = appConfig.app.globalPrefix; app.setGlobalPrefix(globalPrefix); app.useGlobalFilters(new AllExceptionsFilter(app.get(WinstonLogger))); diff --git a/backend/src/microservices/gateways/saas-gateway.ts/base-saas-gateway.service.ts b/backend/src/microservices/gateways/saas-gateway.ts/base-saas-gateway.service.ts index ef842fe44..58fe4caf7 100644 --- a/backend/src/microservices/gateways/saas-gateway.ts/base-saas-gateway.service.ts +++ b/backend/src/microservices/gateways/saas-gateway.ts/base-saas-gateway.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import * as Sentry from '@sentry/node'; import { isSaaS } from '../../../helpers/app/is-saas.js'; +import { appConfig } from '../../../shared/config/app-config.js'; import { generateSaaSJwt } from './utils/generate-saas-jwt.js'; export type SaaSRequestMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'; @@ -11,7 +12,7 @@ export type SaaSResponse = { @Injectable() export class BaseSaasGatewayService { - private readonly baseSaaSUrl = process.env.SAAS_URL || 'http://rocketadmin-private-microservice:3001'; + private readonly baseSaaSUrl = appConfig.thirdParty.saasUrl; async sendRequestToSaaS( patch: string, diff --git a/backend/src/microservices/gateways/saas-gateway.ts/utils/generate-saas-jwt.ts b/backend/src/microservices/gateways/saas-gateway.ts/utils/generate-saas-jwt.ts index 7fabd28ac..0673d0553 100644 --- a/backend/src/microservices/gateways/saas-gateway.ts/utils/generate-saas-jwt.ts +++ b/backend/src/microservices/gateways/saas-gateway.ts/utils/generate-saas-jwt.ts @@ -1,12 +1,15 @@ import jwt from 'jsonwebtoken'; -import { getRequiredEnvVariable } from '../../../../helpers/app/get-requeired-env-variable.js'; +import { appConfig } from '../../../../shared/config/app-config.js'; import { generateRequestId } from './generate-request-id.js'; export function generateSaaSJwt(): string { const today = new Date(); const exp = new Date(today); exp.setDate(today.getDate() + 60); - const secret = getRequiredEnvVariable('MICROSERVICE_JWT_SECRET'); + const secret = appConfig.auth.microserviceJwtSecret; + if (!secret) { + throw new Error('Environment variable MICROSERVICE_JWT_SECRET is not set'); + } const requestId = generateRequestId(); return jwt.sign( { diff --git a/backend/src/shared/config/app-config.ts b/backend/src/shared/config/app-config.ts new file mode 100644 index 000000000..0a165312b --- /dev/null +++ b/backend/src/shared/config/app-config.ts @@ -0,0 +1,345 @@ +import { uuid_ossp } from '@electric-sql/pglite/contrib/uuid_ossp'; +import dotenv from 'dotenv'; +import fs from 'fs'; +import path, { join } from 'path'; +import parse from 'pg-connection-string'; +import { DataSourceOptions } from 'typeorm'; +import { PGliteDriver } from 'typeorm-pglite'; +import { fileURLToPath } from 'url'; + +dotenv.config(); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export interface AuthConfig { + privateKey: string | null; + jwtSecret: string | null; + temporaryJwtSecret: string | null; + microserviceJwtSecret: string | null; + basicAuthLogin: string | null; + basicAuthPassword: string | null; +} + +export interface AppSectionConfig { + port: number; + globalPrefix: string; + logLevel: string; + domainAddress: string; + cookieDomain: string | null; +} + +export interface DbConfig { + databaseUrl: string | null; + pgliteFolderPath: string | null; +} + +export interface EmailConfig { + configString: string | null; + host: string | null; + port: number; + username: string | null; + password: string | null; + nonSecure: boolean; + from: string; +} + +export interface ThirdPartyConfig { + sentryDsn: string | null; + intercomKey: string | null; + amplitudeApiKey: string | null; + slackBotAccessToken: string | null; + turnstileSecretKey: string | null; + saasUrl: string; +} + +export interface TestDbConfig { + testConnectionsJson: string | null; + postgres: { + host: string | null; + port: number | null; + username: string | null; + password: string | null; + database: string | null; + }; + mssql: { + host: string | null; + port: number | null; + username: string | null; + password: string | null; + database: string | null; + }; + oracle: { + host: string | null; + port: number | null; + username: string | null; + password: string | null; + database: string | null; + sid: string | null; + }; + mysql: { + host: string | null; + port: number | null; + username: string | null; + password: string | null; + database: string | null; + sshHost: string | null; + sshPort: string | null; + sshUsername: string | null; + sshKey: string | null; + }; + mongo: { + host: string | null; + port: number | null; + username: string | null; + password: string | null; + database: string | null; + authSource: string | null; + }; + ibmdb2: { + host: string | null; + port: number | null; + username: string | null; + password: string | null; + database: string | null; + schema: string | null; + }; +} + +const AUTOADMIN_SUPPORT_MAIL = 'support@autoadmin.org'; +const DEFAULT_APP_DOMAIN_ADDRESS = 'http://127.0.0.1:3000'; +const DEFAULT_SAAS_URL = 'http://rocketadmin-private-microservice:3001'; +const DEFAULT_LOG_LEVEL = 'info'; +const DEFAULT_GLOBAL_PREFIX = '/'; +const DEFAULT_EMAIL_PORT = 25; +const DEFAULT_PORT = 3000; + +function readString(key: string): string | null { + // eslint-disable-next-line security/detect-object-injection + const value = process.env[key]; + return value && value.length > 0 ? value : null; +} + +function readInt(key: string): number | null { + const raw = readString(key); + if (raw === null) return null; + const parsed = parseInt(raw, 10); + return Number.isFinite(parsed) ? parsed : null; +} + +export class AppConfig { + public readonly auth: AuthConfig; + public readonly app: AppSectionConfig; + public readonly db: DbConfig; + public readonly email: EmailConfig; + public readonly thirdParty: ThirdPartyConfig; + public readonly testDb: TestDbConfig; + + constructor() { + this.auth = Object.freeze({ + privateKey: readString('PRIVATE_KEY'), + jwtSecret: readString('JWT_SECRET'), + temporaryJwtSecret: readString('TEMPORARY_JWT_SECRET'), + microserviceJwtSecret: readString('MICROSERVICE_JWT_SECRET'), + basicAuthLogin: readString('BASIC_AUTH_LOGIN'), + basicAuthPassword: readString('BASIC_AUTH_PWD'), + }); + + this.app = Object.freeze({ + port: readInt('PORT') ?? DEFAULT_PORT, + globalPrefix: readString('GLOBAL_PREFIX') ?? DEFAULT_GLOBAL_PREFIX, + logLevel: readString('LOG_LEVEL') ?? DEFAULT_LOG_LEVEL, + domainAddress: readString('APP_DOMAIN_ADDRESS') ?? DEFAULT_APP_DOMAIN_ADDRESS, + cookieDomain: readString('ROCKETADMIN_COOKIE_DOMAIN'), + }); + + this.db = Object.freeze({ + databaseUrl: readString('DATABASE_URL'), + pgliteFolderPath: readString('PGLITE_FOLDER_PATH'), + }); + + this.email = Object.freeze({ + configString: readString('EMAIL_CONFIG_STRING'), + host: readString('EMAIL_SERVICE_HOST'), + port: readInt('EMAIL_SERVICE_PORT') ?? DEFAULT_EMAIL_PORT, + username: readString('EMAIL_SERVICE_USERNAME'), + password: readString('EMAIL_SERVICE_PASSWORD'), + nonSecure: readString('NON_SSL_EMAIL') === null, + from: readString('EMAIL_FROM') ?? AUTOADMIN_SUPPORT_MAIL, + }); + + this.thirdParty = Object.freeze({ + sentryDsn: readString('SENTRY_DSN'), + intercomKey: readString('INTERCOM_KEY'), + amplitudeApiKey: readString('AMPLITUDE_API_KEY'), + slackBotAccessToken: readString('SLACK_BOT_ACCESS_TOKEN'), + turnstileSecretKey: readString('TURNSTILE_SECRET_KEY'), + saasUrl: readString('SAAS_URL') ?? DEFAULT_SAAS_URL, + }); + + this.testDb = Object.freeze({ + testConnectionsJson: readString('TEST_CONNECTIONS'), + postgres: Object.freeze({ + host: readString('POSTGRES_CONNECTION_HOST'), + port: readInt('POSTGRES_CONNECTION_PORT'), + username: readString('POSTGRES_CONNECTION_USERNAME'), + password: readString('POSTGRES_CONNECTION_PASSWORD'), + database: readString('POSTGRES_CONNECTION_DATABASE'), + }), + mssql: Object.freeze({ + host: readString('MSSQL_CONNECTION_HOST'), + port: readInt('MSSQL_CONNECTION_PORT'), + username: readString('MSSQL_CONNECTION_USERNAME'), + password: readString('MSSQL_CONNECTION_PASSWORD'), + database: readString('MSSQL_CONNECTION_DATABASE'), + }), + oracle: Object.freeze({ + host: readString('ORACLE_CONNECTION_HOST'), + port: readInt('ORACLE_CONNECTION_PORT'), + username: readString('ORACLE_CONNECTION_USERNAME'), + password: readString('ORACLE_CONNECTION_PASSWORD'), + database: readString('ORACLE_CONNECTION_DATABASE'), + sid: readString('ORACLE_CONNECTION_SID'), + }), + mysql: Object.freeze({ + host: readString('MYSQL_CONNECTION_HOST'), + port: readInt('MYSQL_CONNECTION_PORT'), + username: readString('MYSQL_CONNECTION_USERNAME'), + password: readString('MYSQL_CONNECTION_PASSWORD'), + database: readString('MYSQL_CONNECTION_DATABASE'), + sshHost: readString('MYSQL_CONNECTION_SSH_HOST'), + sshPort: readString('MYSQL_CONNECTION_SSH_PORT'), + sshUsername: readString('MYSQL_CONNECTION_SSH_USERNAME'), + sshKey: readString('MYSQL_CONNECTION_SSH_KEY'), + }), + mongo: Object.freeze({ + host: readString('MONGO_CONNECTION_HOST'), + port: readInt('MONGO_CONNECTION_PORT'), + username: readString('MONGO_CONNECTION_USERNAME'), + password: readString('MONGO_CONNECTION_PASSWORD'), + database: readString('MONGO_CONNECTION_DATABASE'), + authSource: readString('MONGO_CONNECTION_AUTH_SOURCE'), + }), + ibmdb2: Object.freeze({ + host: readString('IBM_DB2_CONNECTION_HOST'), + port: readInt('IBM_DB2_CONNECTION_PORT'), + username: readString('IBM_DB2_CONNECTION_USERNAME'), + password: readString('IBM_DB2_CONNECTION_PASSWORD'), + database: readString('IBM_DB2_CONNECTION_DATABASE'), + schema: readString('IBM_DB2_CONNECTION_SCHEMA'), + }), + }); + + Object.freeze(this); + } + + public get isTest(): boolean { + return process.env.NODE_ENV === 'test'; + } + + public get isSaaS(): boolean { + return !!process.env.IS_SAAS; + } + + public validate(): void { + if (this.isTest) { + console.info('Running test environment'); + return; + } + + const missing: Array = []; + if (!this.auth.privateKey) missing.push('PRIVATE_KEY'); + if (!this.auth.jwtSecret) missing.push('JWT_SECRET'); + if (!this.db.pgliteFolderPath && !this.db.databaseUrl) missing.push('DATABASE_URL'); + + if (missing.length > 0) { + const plural = missing.length > 1; + throw new Error(`Required parameter${plural ? 's' : ''} ${missing.join(', ')} ${plural ? 'are' : 'is'} missing`); + } + } + + public getTypeOrmConfig(): DataSourceOptions { + let pgLiteDriver = null; + let connectionParams = {}; + + const pgLiteFolderPath = this.db.pgliteFolderPath; + if (pgLiteFolderPath && pgLiteFolderPath.length > 0) { + const fullPath = this.isTest + ? path.join(process.cwd(), ...pgLiteFolderPath.split('/')) + : path.join(__dirname, '..', '..', '..', pgLiteFolderPath); + console.info('\nPg Lite Folder Patch: ', pgLiteFolderPath, '\n'); + const resolvedPath = path.resolve(fullPath); + try { + fs.accessSync(resolvedPath, fs.constants.F_OK); + console.log('PGLite directory exists'); + try { + fs.accessSync(resolvedPath, fs.constants.W_OK); + console.log('PGLite directory is writable'); + } catch (writeError) { + console.warn('PGLite directory exists but may not be writable:', writeError.message); + } + } catch (error) { + console.log('PGLite directory does not exist, will be created by PGLite', error); + } + + pgLiteDriver = new PGliteDriver({ + extensions: { uuid_ossp }, + dataDir: path.resolve(resolvedPath), + }).driver; + } else { + if (!this.db.databaseUrl) { + throw new Error('DATABASE_URL is required when PGLITE_FOLDER_PATH is not set'); + } + connectionParams = this.parseTypeORMUrl(this.db.databaseUrl); + } + + const baseConfig: DataSourceOptions = { + type: 'postgres', + ...connectionParams, + entities: [join(__dirname, '..', '..', '**', '*.entity.{ts,js}')], + migrations: [join(__dirname, '..', '..', 'migrations', '*.{ts,js}')], + synchronize: false, + migrationsRun: false, + driver: pgLiteDriver ? pgLiteDriver : undefined, + }; + + if (this.isTest) { + return { + ...baseConfig, + logging: false, + logger: 'advanced-console', + extra: { max: 10 }, + }; + } + + return { + ...baseConfig, + extra: { + max: 20, + idle_in_transaction_session_timeout: 20 * 1000, + }, + }; + } + + private parseTypeORMUrl(url: string): { + host: string; + port: number; + username: string; + password: string; + database: string; + ssl: any; + } { + const parsingResult = parse.parse(url); + const { host, port, user, password, database, ssl } = parsingResult; + return { + host, + port: parseInt(port, 10), + username: user, + password, + database, + ssl, + }; + } +} + +export const appConfig = new AppConfig(); diff --git a/backend/src/shared/config/config.module.ts b/backend/src/shared/config/config.module.ts new file mode 100644 index 000000000..a4ea5064b --- /dev/null +++ b/backend/src/shared/config/config.module.ts @@ -0,0 +1,19 @@ +import { Global, Module } from '@nestjs/common'; +import { appConfig } from './app-config.js'; +import { ConfigService } from './config.service.js'; + +@Global() +@Module({ + providers: [ + { + provide: ConfigService, + useFactory: () => { + const service = new ConfigService(); + appConfig.validate(); + return service; + }, + }, + ], + exports: [ConfigService], +}) +export class ConfigModule {} diff --git a/backend/src/shared/config/config.service.ts b/backend/src/shared/config/config.service.ts index 988a66bc7..bb97e707d 100644 --- a/backend/src/shared/config/config.service.ts +++ b/backend/src/shared/config/config.service.ts @@ -1,136 +1,56 @@ -import dotenv from 'dotenv'; -import path, { join } from 'path'; -import parse from 'pg-connection-string'; +import { Injectable } from '@nestjs/common'; import { DataSourceOptions } from 'typeorm'; -import { fileURLToPath } from 'url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -import { uuid_ossp } from '@electric-sql/pglite/contrib/uuid_ossp'; -import fs from 'fs'; -import { PGliteDriver } from 'typeorm-pglite'; -import { isTest } from '../../helpers/app/is-test.js'; +import { + AppSectionConfig, + AuthConfig, + appConfig, + DbConfig, + EmailConfig, + TestDbConfig, + ThirdPartyConfig, +} from './app-config.js'; + +@Injectable() +export class ConfigService { + public get auth(): AuthConfig { + return appConfig.auth; + } -dotenv.config(); + public get app(): AppSectionConfig { + return appConfig.app; + } -class ConfigService { - constructor(private env: { [k: string]: string | undefined }) {} + public get db(): DbConfig { + return appConfig.db; + } - private getValue(key: string, throwOnMissing = !this.isTestEnvironment()): string { - // eslint-disable-next-line security/detect-object-injection - const value = this.env[key]; - if (!value && throwOnMissing) { - throw new Error(`config error - missing env.${key}`); - } + public get email(): EmailConfig { + return appConfig.email; + } - return value; + public get thirdParty(): ThirdPartyConfig { + return appConfig.thirdParty; } - public ensureValues(keys: Array) { - const isTest = this.isTestEnvironment(); - if (isTest) { - console.info('Running test environment'); - } - keys.forEach((k) => this.getValue(k, !isTest)); - return this; + public get testDb(): TestDbConfig { + return appConfig.testDb; } - public getPort() { - return this.getValue('PORT', !this.isTestEnvironment()); + public get isTest(): boolean { + return appConfig.isTest; } - public isTestEnvironment(): boolean { - return process.env.NODE_ENV === 'test'; + public get isSaaS(): boolean { + return appConfig.isSaaS; } public getTypeOrmConfig(): DataSourceOptions { - const pgLiteFolderPath = process.env.PGLITE_FOLDER_PATH; - - let pgLiteDriver = null; - let connectionParams = {}; - - if (pgLiteFolderPath && pgLiteFolderPath.length > 0) { - const fullPath = isTest() - ? path.join(process.cwd(), ...pgLiteFolderPath.split('/')) - : path.join(__dirname, '..', '..', '..', pgLiteFolderPath); - console.info('\nPg Lite Folder Patch: ', pgLiteFolderPath, '\n'); - const resolvedPath = path.resolve(fullPath); - try { - fs.accessSync(resolvedPath, fs.constants.F_OK); - console.log('PGLite directory exists'); - try { - fs.accessSync(resolvedPath, fs.constants.W_OK); - console.log('PGLite directory is writable'); - } catch (writeError) { - console.warn('PGLite directory exists but may not be writable:', writeError.message); - } - } catch (error) { - console.log('PGLite directory does not exist, will be created by PGLite', error); - } - - pgLiteDriver = new PGliteDriver({ - extensions: { uuid_ossp }, - dataDir: path.resolve(resolvedPath), - }).driver; - } else { - connectionParams = this.parseTypeORMUrl(this.getValue('DATABASE_URL')); - } - - const newTypeOrmProdConfig: DataSourceOptions = { - type: 'postgres', - ...connectionParams, - entities: [join(__dirname, '..', '..', '**', '*.entity.{ts,js}')], - migrations: [join(__dirname, '..', '..', 'migrations', '*.{ts,js}')], - synchronize: false, - migrationsRun: false, - extra: { - max: 20, - idle_in_transaction_session_timeout: 20 * 1000, - }, - driver: pgLiteDriver ? pgLiteDriver : undefined, - }; - - const newTypeOrmTestConfig: DataSourceOptions = { - type: 'postgres', - ...connectionParams, - entities: [join(__dirname, '..', '..', '**', '*.entity.{ts,js}')], - migrations: [join(__dirname, '..', '..', 'migrations', '*.{ts,js}')], - synchronize: false, - migrationsRun: false, - logging: false, - extra: { - max: 10, - }, - logger: 'advanced-console', - driver: pgLiteDriver ? pgLiteDriver : undefined, - }; - - return this.isTestEnvironment() ? newTypeOrmTestConfig : newTypeOrmProdConfig; + return appConfig.getTypeOrmConfig(); } - private parseTypeORMUrl(url: string): { - host: string; - port: number; - username: string; - password: string; - database: string; - ssl: any; - } { - const parsingResult = parse.parse(url); - const { host, port, user, password, database, ssl } = parsingResult; - - return { - host, - port: parseInt(port, 10), - username: user, - password, - database, - ssl, - }; + public validate(): void { + appConfig.validate(); } } -const configService = new ConfigService(process.env).ensureValues([]); - -export { configService }; +export { appConfig, appConfig as configService }; diff --git a/backend/src/shared/database/database.service.ts b/backend/src/shared/database/database.service.ts index e17146cbf..90970fc22 100644 --- a/backend/src/shared/database/database.service.ts +++ b/backend/src/shared/database/database.service.ts @@ -1,5 +1,6 @@ import { InjectDataSource } from '@nestjs/typeorm'; import { DataSource } from 'typeorm'; +import { isTest } from '../../helpers/app/is-test.js'; export class DatabaseService { constructor( @@ -12,7 +13,7 @@ export class DatabaseService { } public async dropDatabase() { - if (process.env.NODE_ENV !== 'test') { + if (!isTest()) { throw new Error('This method only for testing'); } try { diff --git a/backend/src/shared/services/turnstile.service.ts b/backend/src/shared/services/turnstile.service.ts index 6bd704f29..24def62b8 100644 --- a/backend/src/shared/services/turnstile.service.ts +++ b/backend/src/shared/services/turnstile.service.ts @@ -2,6 +2,7 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import axios from 'axios'; import { isSaaS } from '../../helpers/app/is-saas.js'; import { isTest } from '../../helpers/app/is-test.js'; +import { appConfig } from '../config/app-config.js'; interface TurnstileVerifyResponse { success: boolean; @@ -15,7 +16,7 @@ export class TurnstileService { private readonly verifyUrl = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'; async verifyToken(token: string): Promise { - const secretKey = process.env.TURNSTILE_SECRET_KEY; + const secretKey = appConfig.thirdParty.turnstileSecretKey; if (isTest() || !isSaaS()) { return true;