diff --git a/packages/hoppscotch-backend/src/admin/infra.resolver.ts b/packages/hoppscotch-backend/src/admin/infra.resolver.ts index 0666827fc..1c08e9a2f 100644 --- a/packages/hoppscotch-backend/src/admin/infra.resolver.ts +++ b/packages/hoppscotch-backend/src/admin/infra.resolver.ts @@ -32,7 +32,7 @@ import { EnableAndDisableSSOArgs, InfraConfigArgs, } from 'src/infra-config/input-args'; -import { InfraConfigEnumForClient } from 'src/types/InfraConfig'; +import { InfraConfigEnum } from 'src/types/InfraConfig'; import { ServiceStatus } from 'src/infra-config/helper'; @UseGuards(GqlThrottlerGuard) @@ -274,10 +274,10 @@ export class InfraResolver { async infraConfigs( @Args({ name: 'configNames', - type: () => [InfraConfigEnumForClient], + type: () => [InfraConfigEnum], description: 'Configs to fetch', }) - names: InfraConfigEnumForClient[], + names: InfraConfigEnum[], ) { const infraConfigs = await this.infraConfigService.getMany(names); if (E.isLeft(infraConfigs)) throwErr(infraConfigs.left); diff --git a/packages/hoppscotch-backend/src/auth/auth.module.ts b/packages/hoppscotch-backend/src/auth/auth.module.ts index ccfadbff8..0dc4c737c 100644 --- a/packages/hoppscotch-backend/src/auth/auth.module.ts +++ b/packages/hoppscotch-backend/src/auth/auth.module.ts @@ -12,7 +12,10 @@ import { GithubStrategy } from './strategies/github.strategy'; import { MicrosoftStrategy } from './strategies/microsoft.strategy'; import { AuthProvider, authProviderCheck } from './helper'; import { ConfigModule, ConfigService } from '@nestjs/config'; -import { loadInfraConfiguration } from 'src/infra-config/helper'; +import { + isInfraConfigTablePopulated, + loadInfraConfiguration, +} from 'src/infra-config/helper'; import { InfraConfigModule } from 'src/infra-config/infra-config.module'; @Module({ @@ -34,6 +37,11 @@ import { InfraConfigModule } from 'src/infra-config/infra-config.module'; }) export class AuthModule { static async register() { + const isInfraConfigPopulated = await isInfraConfigTablePopulated(); + if (!isInfraConfigPopulated) { + return { module: AuthModule }; + } + const env = await loadInfraConfiguration(); const allowedAuthProviders = env.INFRA.VITE_ALLOWED_AUTH_PROVIDERS; diff --git a/packages/hoppscotch-backend/src/auth/strategies/github.strategy.ts b/packages/hoppscotch-backend/src/auth/strategies/github.strategy.ts index 3f81e41e0..4c2d3b1ea 100644 --- a/packages/hoppscotch-backend/src/auth/strategies/github.strategy.ts +++ b/packages/hoppscotch-backend/src/auth/strategies/github.strategy.ts @@ -17,8 +17,8 @@ export class GithubStrategy extends PassportStrategy(Strategy) { super({ clientID: configService.get('INFRA.GITHUB_CLIENT_ID'), clientSecret: configService.get('INFRA.GITHUB_CLIENT_SECRET'), - callbackURL: configService.get('GITHUB_CALLBACK_URL'), - scope: [configService.get('GITHUB_SCOPE')], + callbackURL: configService.get('INFRA.GITHUB_CALLBACK_URL'), + scope: [configService.get('INFRA.GITHUB_SCOPE')], store: true, }); } diff --git a/packages/hoppscotch-backend/src/auth/strategies/google.strategy.ts b/packages/hoppscotch-backend/src/auth/strategies/google.strategy.ts index 2a1b92497..52bb77764 100644 --- a/packages/hoppscotch-backend/src/auth/strategies/google.strategy.ts +++ b/packages/hoppscotch-backend/src/auth/strategies/google.strategy.ts @@ -17,8 +17,8 @@ export class GoogleStrategy extends PassportStrategy(Strategy) { super({ clientID: configService.get('INFRA.GOOGLE_CLIENT_ID'), clientSecret: configService.get('INFRA.GOOGLE_CLIENT_SECRET'), - callbackURL: configService.get('GOOGLE_CALLBACK_URL'), - scope: configService.get('GOOGLE_SCOPE').split(','), + callbackURL: configService.get('INFRA.GOOGLE_CALLBACK_URL'), + scope: configService.get('INFRA.GOOGLE_SCOPE').split(','), passReqToCallback: true, store: true, }); diff --git a/packages/hoppscotch-backend/src/auth/strategies/microsoft.strategy.ts b/packages/hoppscotch-backend/src/auth/strategies/microsoft.strategy.ts index a174e2b32..906385f5f 100644 --- a/packages/hoppscotch-backend/src/auth/strategies/microsoft.strategy.ts +++ b/packages/hoppscotch-backend/src/auth/strategies/microsoft.strategy.ts @@ -17,9 +17,9 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy) { super({ clientID: configService.get('INFRA.MICROSOFT_CLIENT_ID'), clientSecret: configService.get('INFRA.MICROSOFT_CLIENT_SECRET'), - callbackURL: configService.get('MICROSOFT_CALLBACK_URL'), - scope: [configService.get('MICROSOFT_SCOPE')], - tenant: configService.get('MICROSOFT_TENANT'), + callbackURL: configService.get('INFRA.MICROSOFT_CALLBACK_URL'), + scope: [configService.get('INFRA.MICROSOFT_SCOPE')], + tenant: configService.get('INFRA.MICROSOFT_TENANT'), store: true, }); } diff --git a/packages/hoppscotch-backend/src/errors.ts b/packages/hoppscotch-backend/src/errors.ts index ef7fbc0df..68a579485 100644 --- a/packages/hoppscotch-backend/src/errors.ts +++ b/packages/hoppscotch-backend/src/errors.ts @@ -731,6 +731,13 @@ export const INFRA_CONFIG_INVALID_INPUT = 'infra_config/invalid_input' as const; export const INFRA_CONFIG_SERVICE_NOT_CONFIGURED = 'infra_config/service_not_configured' as const; +/** + * Infra Config update/fetch operation not allowed + * (InfraConfigService) + */ +export const INFRA_CONFIG_OPERATION_NOT_ALLOWED = + 'infra_config/operation_not_allowed'; + /** * Error message for when the database table does not exist * (InfraConfigService) diff --git a/packages/hoppscotch-backend/src/infra-config/helper.ts b/packages/hoppscotch-backend/src/infra-config/helper.ts index b8d66284e..dcbae99dc 100644 --- a/packages/hoppscotch-backend/src/infra-config/helper.ts +++ b/packages/hoppscotch-backend/src/infra-config/helper.ts @@ -1,5 +1,8 @@ import { AuthProvider } from 'src/auth/helper'; -import { AUTH_PROVIDER_NOT_CONFIGURED } from 'src/errors'; +import { + AUTH_PROVIDER_NOT_CONFIGURED, + DATABASE_TABLE_NOT_EXIST, +} from 'src/errors'; import { PrismaService } from 'src/prisma/prisma.service'; import { InfraConfigEnum } from 'src/types/InfraConfig'; import { throwErr } from 'src/utils'; @@ -14,14 +17,21 @@ const AuthProviderConfigurations = { [AuthProvider.GOOGLE]: [ InfraConfigEnum.GOOGLE_CLIENT_ID, InfraConfigEnum.GOOGLE_CLIENT_SECRET, + InfraConfigEnum.GOOGLE_CALLBACK_URL, + InfraConfigEnum.GOOGLE_SCOPE, ], [AuthProvider.GITHUB]: [ InfraConfigEnum.GITHUB_CLIENT_ID, InfraConfigEnum.GITHUB_CLIENT_SECRET, + InfraConfigEnum.GITHUB_CALLBACK_URL, + InfraConfigEnum.GITHUB_SCOPE, ], [AuthProvider.MICROSOFT]: [ InfraConfigEnum.MICROSOFT_CLIENT_ID, InfraConfigEnum.MICROSOFT_CLIENT_SECRET, + InfraConfigEnum.MICROSOFT_CALLBACK_URL, + InfraConfigEnum.MICROSOFT_SCOPE, + InfraConfigEnum.MICROSOFT_TENANT, ], [AuthProvider.EMAIL]: [ InfraConfigEnum.MAILER_SMTP_URL, @@ -54,6 +64,125 @@ export async function loadInfraConfiguration() { } } +/** + * Read the default values from .env file and return them as an array + * @returns Array of default infra configs + */ +export async function getDefaultInfraConfigs(): Promise< + { name: InfraConfigEnum; value: string }[] +> { + const prisma = new PrismaService(); + + // Prepare rows for 'infra_config' table with default values (from .env) for each 'name' + const infraConfigDefaultObjs: { name: InfraConfigEnum; value: string }[] = [ + { + name: InfraConfigEnum.MAILER_SMTP_URL, + value: process.env.MAILER_SMTP_URL, + }, + { + name: InfraConfigEnum.MAILER_ADDRESS_FROM, + value: process.env.MAILER_ADDRESS_FROM, + }, + { + name: InfraConfigEnum.GOOGLE_CLIENT_ID, + value: process.env.GOOGLE_CLIENT_ID, + }, + { + name: InfraConfigEnum.GOOGLE_CLIENT_SECRET, + value: process.env.GOOGLE_CLIENT_SECRET, + }, + { + name: InfraConfigEnum.GOOGLE_CALLBACK_URL, + value: process.env.GOOGLE_CALLBACK_URL, + }, + { + name: InfraConfigEnum.GOOGLE_SCOPE, + value: process.env.GOOGLE_SCOPE, + }, + { + name: InfraConfigEnum.GITHUB_CLIENT_ID, + value: process.env.GITHUB_CLIENT_ID, + }, + { + name: InfraConfigEnum.GITHUB_CLIENT_SECRET, + value: process.env.GITHUB_CLIENT_SECRET, + }, + { + name: InfraConfigEnum.GITHUB_CALLBACK_URL, + value: process.env.GITHUB_CALLBACK_URL, + }, + { + name: InfraConfigEnum.GITHUB_SCOPE, + value: process.env.GITHUB_SCOPE, + }, + { + name: InfraConfigEnum.MICROSOFT_CLIENT_ID, + value: process.env.MICROSOFT_CLIENT_ID, + }, + { + name: InfraConfigEnum.MICROSOFT_CLIENT_SECRET, + value: process.env.MICROSOFT_CLIENT_SECRET, + }, + { + name: InfraConfigEnum.MICROSOFT_CALLBACK_URL, + value: process.env.MICROSOFT_CALLBACK_URL, + }, + { + name: InfraConfigEnum.MICROSOFT_SCOPE, + value: process.env.MICROSOFT_SCOPE, + }, + { + name: InfraConfigEnum.MICROSOFT_TENANT, + value: process.env.MICROSOFT_TENANT, + }, + { + name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS, + value: getConfiguredSSOProviders(), + }, + { + name: InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION, + value: false.toString(), + }, + { + name: InfraConfigEnum.ANALYTICS_USER_ID, + value: generateAnalyticsUserId(), + }, + { + name: InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP, + value: (await prisma.infraConfig.count()) === 0 ? 'true' : 'false', + }, + ]; + + return infraConfigDefaultObjs; +} + +/** + * Verify if 'infra_config' table is loaded with all entries + * @returns boolean + */ +export async function isInfraConfigTablePopulated(): Promise { + const prisma = new PrismaService(); + try { + const dbInfraConfigs = await prisma.infraConfig.findMany(); + const infraConfigDefaultObjs = await getDefaultInfraConfigs(); + + const propsRemainingToInsert = infraConfigDefaultObjs.filter( + (p) => !dbInfraConfigs.find((e) => e.name === p.name), + ); + + if (propsRemainingToInsert.length > 0) { + console.log( + 'Infra Config table is not populated with all entries. Populating now...', + ); + return false; + } + + return true; + } catch (error) { + return false; + } +} + /** * Stop the app after 5 seconds * (Docker will re-start the app) diff --git a/packages/hoppscotch-backend/src/infra-config/infra-config.controller.ts b/packages/hoppscotch-backend/src/infra-config/infra-config.controller.ts index 92468fe3f..e0b11d78f 100644 --- a/packages/hoppscotch-backend/src/infra-config/infra-config.controller.ts +++ b/packages/hoppscotch-backend/src/infra-config/infra-config.controller.ts @@ -5,7 +5,7 @@ import * as E from 'fp-ts/Either'; import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'; import { RESTAdminGuard } from 'src/admin/guards/rest-admin.guard'; import { RESTError } from 'src/types/RESTError'; -import { InfraConfigEnumForClient } from 'src/types/InfraConfig'; +import { InfraConfigEnum } from 'src/types/InfraConfig'; import { throwHTTPErr } from 'src/utils'; @UseGuards(ThrottlerBehindProxyGuard) @@ -17,7 +17,7 @@ export class SiteController { @UseGuards(JwtAuthGuard, RESTAdminGuard) async fetchSetupInfo() { const status = await this.infraConfigService.get( - InfraConfigEnumForClient.IS_FIRST_TIME_INFRA_SETUP, + InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP, ); if (E.isLeft(status)) @@ -32,7 +32,7 @@ export class SiteController { @UseGuards(JwtAuthGuard, RESTAdminGuard) async setSetupAsComplete() { const res = await this.infraConfigService.update( - InfraConfigEnumForClient.IS_FIRST_TIME_INFRA_SETUP, + InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP, false.toString(), false, ); diff --git a/packages/hoppscotch-backend/src/infra-config/infra-config.model.ts b/packages/hoppscotch-backend/src/infra-config/infra-config.model.ts index 6335261ca..7962c6965 100644 --- a/packages/hoppscotch-backend/src/infra-config/infra-config.model.ts +++ b/packages/hoppscotch-backend/src/infra-config/infra-config.model.ts @@ -1,6 +1,6 @@ import { Field, ObjectType, registerEnumType } from '@nestjs/graphql'; import { AuthProvider } from 'src/auth/helper'; -import { InfraConfigEnumForClient } from 'src/types/InfraConfig'; +import { InfraConfigEnum } from 'src/types/InfraConfig'; import { ServiceStatus } from './helper'; @ObjectType() @@ -8,7 +8,7 @@ export class InfraConfig { @Field({ description: 'Infra Config Name', }) - name: InfraConfigEnumForClient; + name: InfraConfigEnum; @Field({ description: 'Infra Config Value', @@ -16,7 +16,7 @@ export class InfraConfig { value: string; } -registerEnumType(InfraConfigEnumForClient, { +registerEnumType(InfraConfigEnum, { name: 'InfraConfigEnum', }); diff --git a/packages/hoppscotch-backend/src/infra-config/infra-config.service.spec.ts b/packages/hoppscotch-backend/src/infra-config/infra-config.service.spec.ts index 7c236dd55..707294231 100644 --- a/packages/hoppscotch-backend/src/infra-config/infra-config.service.spec.ts +++ b/packages/hoppscotch-backend/src/infra-config/infra-config.service.spec.ts @@ -1,13 +1,16 @@ import { mockDeep, mockReset } from 'jest-mock-extended'; import { PrismaService } from 'src/prisma/prisma.service'; import { InfraConfigService } from './infra-config.service'; +import { InfraConfigEnum } from 'src/types/InfraConfig'; import { - InfraConfigEnum, - InfraConfigEnumForClient, -} from 'src/types/InfraConfig'; -import { INFRA_CONFIG_NOT_FOUND, INFRA_CONFIG_UPDATE_FAILED } from 'src/errors'; + INFRA_CONFIG_NOT_FOUND, + INFRA_CONFIG_OPERATION_NOT_ALLOWED, + INFRA_CONFIG_UPDATE_FAILED, +} from 'src/errors'; import { ConfigService } from '@nestjs/config'; import * as helper from './helper'; +import { InfraConfig as dbInfraConfig } from '@prisma/client'; +import { InfraConfig } from './infra-config.model'; const mockPrisma = mockDeep(); const mockConfigService = mockDeep(); @@ -19,12 +22,82 @@ const infraConfigService = new InfraConfigService( mockConfigService, ); +const INITIALIZED_DATE_CONST = new Date(); +const dbInfraConfigs: dbInfraConfig[] = [ + { + id: '3', + name: InfraConfigEnum.GOOGLE_CLIENT_ID, + value: 'abcdefghijkl', + active: true, + createdOn: INITIALIZED_DATE_CONST, + updatedOn: INITIALIZED_DATE_CONST, + }, + { + id: '4', + name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS, + value: 'google', + active: true, + createdOn: INITIALIZED_DATE_CONST, + updatedOn: INITIALIZED_DATE_CONST, + }, +]; +const infraConfigs: InfraConfig[] = [ + { + name: dbInfraConfigs[0].name as InfraConfigEnum, + value: dbInfraConfigs[0].value, + }, + { + name: dbInfraConfigs[1].name as InfraConfigEnum, + value: dbInfraConfigs[1].value, + }, +]; + beforeEach(() => { mockReset(mockPrisma); }); describe('InfraConfigService', () => { describe('update', () => { + it('should update the infra config without backend server restart', async () => { + const name = InfraConfigEnum.GOOGLE_CLIENT_ID; + const value = 'true'; + + mockPrisma.infraConfig.update.mockResolvedValueOnce({ + id: '', + name, + value, + active: true, + createdOn: new Date(), + updatedOn: new Date(), + }); + + jest.spyOn(helper, 'stopApp').mockReturnValueOnce(); + const result = await infraConfigService.update(name, value); + + expect(helper.stopApp).not.toHaveBeenCalled(); + expect(result).toEqualRight({ name, value }); + }); + + it('should update the infra config with backend server restart', async () => { + const name = InfraConfigEnum.GOOGLE_CLIENT_ID; + const value = 'true'; + + mockPrisma.infraConfig.update.mockResolvedValueOnce({ + id: '', + name, + value, + active: true, + createdOn: new Date(), + updatedOn: new Date(), + }); + jest.spyOn(helper, 'stopApp').mockReturnValueOnce(); + + const result = await infraConfigService.update(name, value, true); + + expect(helper.stopApp).toHaveBeenCalledTimes(1); + expect(result).toEqualRight({ name, value }); + }); + it('should update the infra config', async () => { const name = InfraConfigEnum.GOOGLE_CLIENT_ID; const value = 'true'; @@ -71,7 +144,7 @@ describe('InfraConfigService', () => { describe('get', () => { it('should get the infra config', async () => { - const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID; + const name = InfraConfigEnum.GOOGLE_CLIENT_ID; const value = 'true'; mockPrisma.infraConfig.findUniqueOrThrow.mockResolvedValueOnce({ @@ -87,7 +160,7 @@ describe('InfraConfigService', () => { }); it('should pass correct params to prisma findUnique', async () => { - const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID; + const name = InfraConfigEnum.GOOGLE_CLIENT_ID; await infraConfigService.get(name); @@ -98,7 +171,7 @@ describe('InfraConfigService', () => { }); it('should throw an error if the infra config does not exist', async () => { - const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID; + const name = InfraConfigEnum.GOOGLE_CLIENT_ID; mockPrisma.infraConfig.findUniqueOrThrow.mockRejectedValueOnce('null'); @@ -106,4 +179,45 @@ describe('InfraConfigService', () => { expect(result).toEqualLeft(INFRA_CONFIG_NOT_FOUND); }); }); + + describe('getMany', () => { + it('should throw error if any disallowed names are provided', async () => { + const disallowedNames = [InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS]; + const result = await infraConfigService.getMany(disallowedNames); + + expect(result).toEqualLeft(INFRA_CONFIG_OPERATION_NOT_ALLOWED); + }); + it('should resolve right with disallowed names if `checkDisallowed` parameter passed', async () => { + const disallowedNames = [InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS]; + + const dbInfraConfigResponses = dbInfraConfigs.filter((dbConfig) => + disallowedNames.includes(dbConfig.name as InfraConfigEnum), + ); + mockPrisma.infraConfig.findMany.mockResolvedValueOnce( + dbInfraConfigResponses, + ); + + const result = await infraConfigService.getMany(disallowedNames, false); + + expect(result).toEqualRight( + infraConfigs.filter((i) => disallowedNames.includes(i.name)), + ); + }); + + it('should return right with infraConfigs if Prisma query succeeds', async () => { + const allowedNames = [InfraConfigEnum.GOOGLE_CLIENT_ID]; + + const dbInfraConfigResponses = dbInfraConfigs.filter((dbConfig) => + allowedNames.includes(dbConfig.name as InfraConfigEnum), + ); + mockPrisma.infraConfig.findMany.mockResolvedValueOnce( + dbInfraConfigResponses, + ); + + const result = await infraConfigService.getMany(allowedNames); + expect(result).toEqualRight( + infraConfigs.filter((i) => allowedNames.includes(i.name)), + ); + }); + }); }); diff --git a/packages/hoppscotch-backend/src/infra-config/infra-config.service.ts b/packages/hoppscotch-backend/src/infra-config/infra-config.service.ts index e0b4ab59b..a024d9f33 100644 --- a/packages/hoppscotch-backend/src/infra-config/infra-config.service.ts +++ b/packages/hoppscotch-backend/src/infra-config/infra-config.service.ts @@ -3,28 +3,25 @@ import { InfraConfig } from './infra-config.model'; import { PrismaService } from 'src/prisma/prisma.service'; import { InfraConfig as DBInfraConfig } from '@prisma/client'; import * as E from 'fp-ts/Either'; -import { - InfraConfigEnum, - InfraConfigEnumForClient, -} from 'src/types/InfraConfig'; +import { InfraConfigEnum } from 'src/types/InfraConfig'; import { AUTH_PROVIDER_NOT_SPECIFIED, DATABASE_TABLE_NOT_EXIST, INFRA_CONFIG_INVALID_INPUT, INFRA_CONFIG_NOT_FOUND, - INFRA_CONFIG_NOT_LISTED, INFRA_CONFIG_RESET_FAILED, INFRA_CONFIG_UPDATE_FAILED, INFRA_CONFIG_SERVICE_NOT_CONFIGURED, + INFRA_CONFIG_OPERATION_NOT_ALLOWED, } from 'src/errors'; -import { throwErr, validateSMTPEmail, validateSMTPUrl } from 'src/utils'; -import { ConfigService } from '@nestjs/config'; import { - ServiceStatus, - generateAnalyticsUserId, - getConfiguredSSOProviders, - stopApp, -} from './helper'; + throwErr, + validateSMTPEmail, + validateSMTPUrl, + validateUrl, +} from 'src/utils'; +import { ConfigService } from '@nestjs/config'; +import { ServiceStatus, getDefaultInfraConfigs, stopApp } from './helper'; import { EnableAndDisableSSOArgs, InfraConfigArgs } from './input-args'; import { AuthProvider } from 'src/auth/helper'; @@ -35,84 +32,32 @@ export class InfraConfigService implements OnModuleInit { private readonly configService: ConfigService, ) {} + // Following fields are not updatable by `infraConfigs` Mutation. Use dedicated mutations for these fields instead. + EXCLUDE_FROM_UPDATE_CONFIGS = [ + InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS, + InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION, + InfraConfigEnum.ANALYTICS_USER_ID, + InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP, + ]; + // Following fields can not be fetched by `infraConfigs` Query. Use dedicated queries for these fields instead. + EXCLUDE_FROM_FETCH_CONFIGS = [ + InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS, + InfraConfigEnum.ANALYTICS_USER_ID, + InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP, + ]; + async onModuleInit() { await this.initializeInfraConfigTable(); } - async getDefaultInfraConfigs(): Promise< - { name: InfraConfigEnum; value: string }[] - > { - // Prepare rows for 'infra_config' table with default values (from .env) for each 'name' - const infraConfigDefaultObjs: { name: InfraConfigEnum; value: string }[] = [ - { - name: InfraConfigEnum.MAILER_SMTP_URL, - value: process.env.MAILER_SMTP_URL, - }, - { - name: InfraConfigEnum.MAILER_ADDRESS_FROM, - value: process.env.MAILER_ADDRESS_FROM, - }, - { - name: InfraConfigEnum.GOOGLE_CLIENT_ID, - value: process.env.GOOGLE_CLIENT_ID, - }, - { - name: InfraConfigEnum.GOOGLE_CLIENT_SECRET, - value: process.env.GOOGLE_CLIENT_SECRET, - }, - { - name: InfraConfigEnum.GITHUB_CLIENT_ID, - value: process.env.GITHUB_CLIENT_ID, - }, - { - name: InfraConfigEnum.GITHUB_CLIENT_SECRET, - value: process.env.GITHUB_CLIENT_SECRET, - }, - { - name: InfraConfigEnum.MICROSOFT_CLIENT_ID, - value: process.env.MICROSOFT_CLIENT_ID, - }, - { - name: InfraConfigEnum.MICROSOFT_CLIENT_SECRET, - value: process.env.MICROSOFT_CLIENT_SECRET, - }, - { - name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS, - value: getConfiguredSSOProviders(), - }, - { - name: InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION, - value: false.toString(), - }, - { - name: InfraConfigEnum.ANALYTICS_USER_ID, - value: generateAnalyticsUserId(), - }, - { - name: InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP, - value: (await this.prisma.infraConfig.count()) === 0 ? 'true' : 'false', - }, - ]; - - return infraConfigDefaultObjs; - } - /** * Initialize the 'infra_config' table with values from .env * @description This function create rows 'infra_config' in very first time (only once) */ async initializeInfraConfigTable() { try { - // Get all the 'names' of the properties to be saved in the 'infra_config' table - const enumValues = Object.values(InfraConfigEnum); - // Fetch the default values (value in .env) for configs to be saved in 'infra_config' table - const infraConfigDefaultObjs = await this.getDefaultInfraConfigs(); - - // Check if all the 'names' are listed in the default values - if (enumValues.length !== infraConfigDefaultObjs.length) { - throw new Error(INFRA_CONFIG_NOT_LISTED); - } + const infraConfigDefaultObjs = await getDefaultInfraConfigs(); // Eliminate the rows (from 'infraConfigDefaultObjs') that are already present in the database table const dbInfraConfigs = await this.prisma.infraConfig.findMany(); @@ -169,11 +114,7 @@ export class InfraConfigService implements OnModuleInit { * @param restartEnabled If true, restart the app after updating the InfraConfig * @returns InfraConfig model */ - async update( - name: InfraConfigEnumForClient | InfraConfigEnum, - value: string, - restartEnabled = false, - ) { + async update(name: InfraConfigEnum, value: string, restartEnabled = false) { const isValidate = this.validateEnvValues([{ name, value }]); if (E.isLeft(isValidate)) return E.left(isValidate.left); @@ -197,6 +138,11 @@ export class InfraConfigService implements OnModuleInit { * @returns InfraConfig model */ async updateMany(infraConfigs: InfraConfigArgs[]) { + for (let i = 0; i < infraConfigs.length; i++) { + if (this.EXCLUDE_FROM_UPDATE_CONFIGS.includes(infraConfigs[i].name)) + return E.left(INFRA_CONFIG_OPERATION_NOT_ALLOWED); + } + const isValidate = this.validateEnvValues(infraConfigs); if (E.isLeft(isValidate)) return E.left(isValidate.left); @@ -230,12 +176,26 @@ export class InfraConfigService implements OnModuleInit { ) { switch (service) { case AuthProvider.GOOGLE: - return configMap.GOOGLE_CLIENT_ID && configMap.GOOGLE_CLIENT_SECRET; + return ( + configMap.GOOGLE_CLIENT_ID && + configMap.GOOGLE_CLIENT_SECRET && + configMap.GOOGLE_CALLBACK_URL && + configMap.GOOGLE_SCOPE + ); case AuthProvider.GITHUB: - return configMap.GITHUB_CLIENT_ID && configMap.GITHUB_CLIENT_SECRET; + return ( + configMap.GITHUB_CLIENT_ID && + configMap.GITHUB_CLIENT_SECRET && + configMap.GITHUB_CALLBACK_URL && + configMap.GITHUB_SCOPE + ); case AuthProvider.MICROSOFT: return ( - configMap.MICROSOFT_CLIENT_ID && configMap.MICROSOFT_CLIENT_SECRET + configMap.MICROSOFT_CLIENT_ID && + configMap.MICROSOFT_CLIENT_SECRET && + configMap.MICROSOFT_CALLBACK_URL && + configMap.MICROSOFT_SCOPE && + configMap.MICROSOFT_TENANT ); case AuthProvider.EMAIL: return configMap.MAILER_SMTP_URL && configMap.MAILER_ADDRESS_FROM; @@ -310,7 +270,7 @@ export class InfraConfigService implements OnModuleInit { * @param name Name of the InfraConfig * @returns InfraConfig model */ - async get(name: InfraConfigEnumForClient) { + async get(name: InfraConfigEnum) { try { const infraConfig = await this.prisma.infraConfig.findUniqueOrThrow({ where: { name }, @@ -327,7 +287,15 @@ export class InfraConfigService implements OnModuleInit { * @param names Names of the InfraConfigs * @returns InfraConfig model */ - async getMany(names: InfraConfigEnumForClient[]) { + async getMany(names: InfraConfigEnum[], checkDisallowedKeys: boolean = true) { + if (checkDisallowedKeys) { + // Check if the names are allowed to fetch by client + for (let i = 0; i < names.length; i++) { + if (this.EXCLUDE_FROM_FETCH_CONFIGS.includes(names[i])) + return E.left(INFRA_CONFIG_OPERATION_NOT_ALLOWED); + } + } + try { const infraConfigs = await this.prisma.infraConfig.findMany({ where: { name: { in: names } }, @@ -354,7 +322,7 @@ export class InfraConfigService implements OnModuleInit { */ async reset() { try { - const infraConfigDefaultObjs = await this.getDefaultInfraConfigs(); + const infraConfigDefaultObjs = await getDefaultInfraConfigs(); await this.prisma.infraConfig.deleteMany({ where: { name: { in: infraConfigDefaultObjs.map((p) => p.name) } }, @@ -387,36 +355,60 @@ export class InfraConfigService implements OnModuleInit { */ validateEnvValues( infraConfigs: { - name: InfraConfigEnumForClient | InfraConfigEnum; + name: InfraConfigEnum; value: string; }[], ) { for (let i = 0; i < infraConfigs.length; i++) { switch (infraConfigs[i].name) { - case InfraConfigEnumForClient.MAILER_SMTP_URL: + case InfraConfigEnum.MAILER_SMTP_URL: const isValidUrl = validateSMTPUrl(infraConfigs[i].value); if (!isValidUrl) return E.left(INFRA_CONFIG_INVALID_INPUT); break; - case InfraConfigEnumForClient.MAILER_ADDRESS_FROM: + case InfraConfigEnum.MAILER_ADDRESS_FROM: const isValidEmail = validateSMTPEmail(infraConfigs[i].value); if (!isValidEmail) return E.left(INFRA_CONFIG_INVALID_INPUT); break; - case InfraConfigEnumForClient.GOOGLE_CLIENT_ID: + case InfraConfigEnum.GOOGLE_CLIENT_ID: if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT); break; - case InfraConfigEnumForClient.GOOGLE_CLIENT_SECRET: + case InfraConfigEnum.GOOGLE_CLIENT_SECRET: if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT); break; - case InfraConfigEnumForClient.GITHUB_CLIENT_ID: + case InfraConfigEnum.GOOGLE_CALLBACK_URL: + if (!validateUrl(infraConfigs[i].value)) + return E.left(INFRA_CONFIG_INVALID_INPUT); + break; + case InfraConfigEnum.GOOGLE_SCOPE: if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT); break; - case InfraConfigEnumForClient.GITHUB_CLIENT_SECRET: + case InfraConfigEnum.GITHUB_CLIENT_ID: if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT); break; - case InfraConfigEnumForClient.MICROSOFT_CLIENT_ID: + case InfraConfigEnum.GITHUB_CLIENT_SECRET: if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT); break; - case InfraConfigEnumForClient.MICROSOFT_CLIENT_SECRET: + case InfraConfigEnum.GITHUB_CALLBACK_URL: + if (!validateUrl(infraConfigs[i].value)) + return E.left(INFRA_CONFIG_INVALID_INPUT); + break; + case InfraConfigEnum.GITHUB_SCOPE: + if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT); + break; + case InfraConfigEnum.MICROSOFT_CLIENT_ID: + if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT); + break; + case InfraConfigEnum.MICROSOFT_CLIENT_SECRET: + if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT); + break; + case InfraConfigEnum.MICROSOFT_CALLBACK_URL: + if (!validateUrl(infraConfigs[i].value)) + return E.left(INFRA_CONFIG_INVALID_INPUT); + break; + case InfraConfigEnum.MICROSOFT_SCOPE: + if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT); + break; + case InfraConfigEnum.MICROSOFT_TENANT: if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT); break; default: diff --git a/packages/hoppscotch-backend/src/infra-config/input-args.ts b/packages/hoppscotch-backend/src/infra-config/input-args.ts index 6b4c26850..fd248e526 100644 --- a/packages/hoppscotch-backend/src/infra-config/input-args.ts +++ b/packages/hoppscotch-backend/src/infra-config/input-args.ts @@ -1,14 +1,14 @@ import { Field, InputType } from '@nestjs/graphql'; -import { InfraConfigEnumForClient } from 'src/types/InfraConfig'; +import { InfraConfigEnum } from 'src/types/InfraConfig'; import { ServiceStatus } from './helper'; import { AuthProvider } from 'src/auth/helper'; @InputType() export class InfraConfigArgs { - @Field(() => InfraConfigEnumForClient, { + @Field(() => InfraConfigEnum, { description: 'Infra Config Name', }) - name: InfraConfigEnumForClient; + name: InfraConfigEnum; @Field({ description: 'Infra Config Value', diff --git a/packages/hoppscotch-backend/src/types/InfraConfig.ts b/packages/hoppscotch-backend/src/types/InfraConfig.ts index 3836194e5..8c02929bc 100644 --- a/packages/hoppscotch-backend/src/types/InfraConfig.ts +++ b/packages/hoppscotch-backend/src/types/InfraConfig.ts @@ -4,12 +4,19 @@ export enum InfraConfigEnum { GOOGLE_CLIENT_ID = 'GOOGLE_CLIENT_ID', GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET', + GOOGLE_CALLBACK_URL = 'GOOGLE_CALLBACK_URL', + GOOGLE_SCOPE = 'GOOGLE_SCOPE', GITHUB_CLIENT_ID = 'GITHUB_CLIENT_ID', GITHUB_CLIENT_SECRET = 'GITHUB_CLIENT_SECRET', + GITHUB_CALLBACK_URL = 'GITHUB_CALLBACK_URL', + GITHUB_SCOPE = 'GITHUB_SCOPE', MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID', MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET', + MICROSOFT_CALLBACK_URL = 'MICROSOFT_CALLBACK_URL', + MICROSOFT_SCOPE = 'MICROSOFT_SCOPE', + MICROSOFT_TENANT = 'MICROSOFT_TENANT', VITE_ALLOWED_AUTH_PROVIDERS = 'VITE_ALLOWED_AUTH_PROVIDERS', @@ -17,20 +24,3 @@ export enum InfraConfigEnum { ANALYTICS_USER_ID = 'ANALYTICS_USER_ID', IS_FIRST_TIME_INFRA_SETUP = 'IS_FIRST_TIME_INFRA_SETUP', } - -export enum InfraConfigEnumForClient { - MAILER_SMTP_URL = 'MAILER_SMTP_URL', - MAILER_ADDRESS_FROM = 'MAILER_ADDRESS_FROM', - - GOOGLE_CLIENT_ID = 'GOOGLE_CLIENT_ID', - GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET', - - GITHUB_CLIENT_ID = 'GITHUB_CLIENT_ID', - GITHUB_CLIENT_SECRET = 'GITHUB_CLIENT_SECRET', - - MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID', - MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET', - - ALLOW_ANALYTICS_COLLECTION = 'ALLOW_ANALYTICS_COLLECTION', - IS_FIRST_TIME_INFRA_SETUP = 'IS_FIRST_TIME_INFRA_SETUP', -} diff --git a/packages/hoppscotch-backend/src/utils.ts b/packages/hoppscotch-backend/src/utils.ts index 043bd46f9..ea809a4ed 100644 --- a/packages/hoppscotch-backend/src/utils.ts +++ b/packages/hoppscotch-backend/src/utils.ts @@ -183,6 +183,16 @@ export const validateSMTPUrl = (url: string) => { return false; }; +/** + * Checks to see if the URL is valid or not + * @param url The URL to validate + * @returns boolean + */ +export const validateUrl = (url: string) => { + const urlRegex = /^(http|https):\/\/[^ "]+$/; + return urlRegex.test(url); +}; + /** * String to JSON parser * @param {str} str The string to parse