diff --git a/packages/hoppscotch-backend/src/errors.ts b/packages/hoppscotch-backend/src/errors.ts index c98ce6062..3297be8f8 100644 --- a/packages/hoppscotch-backend/src/errors.ts +++ b/packages/hoppscotch-backend/src/errors.ts @@ -28,6 +28,13 @@ export const JSON_INVALID = 'json_invalid'; */ export const AUTH_PROVIDER_NOT_SPECIFIED = 'auth/provider_not_specified'; +/** + * Auth Provider not specified + * (Auth) + */ +export const AUTH_PROVIDER_NOT_CONFIGURED = + 'auth/provider_not_configured_correctly'; + /** * Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" is not present in .env file */ diff --git a/packages/hoppscotch-backend/src/infra-config/helper.ts b/packages/hoppscotch-backend/src/infra-config/helper.ts index e053d767e..4e8d5ee3a 100644 --- a/packages/hoppscotch-backend/src/infra-config/helper.ts +++ b/packages/hoppscotch-backend/src/infra-config/helper.ts @@ -1,10 +1,33 @@ +import { AuthProvider } from 'src/auth/helper'; +import { AUTH_PROVIDER_NOT_CONFIGURED } from 'src/errors'; import { PrismaService } from 'src/prisma/prisma.service'; +import { InfraConfigEnum } from 'src/types/InfraConfig'; +import { throwErr } from 'src/utils'; export enum ServiceStatus { ENABLE = 'ENABLE', DISABLE = 'DISABLE', } +const AuthProviderConfigurations = { + [AuthProvider.GOOGLE]: [ + InfraConfigEnum.GOOGLE_CLIENT_ID, + InfraConfigEnum.GOOGLE_CLIENT_SECRET, + ], + [AuthProvider.GITHUB]: [ + InfraConfigEnum.GITHUB_CLIENT_ID, + InfraConfigEnum.GITHUB_CLIENT_SECRET, + ], + [AuthProvider.MICROSOFT]: [ + InfraConfigEnum.MICROSOFT_CLIENT_ID, + InfraConfigEnum.MICROSOFT_CLIENT_SECRET, + ], + [AuthProvider.EMAIL]: [ + InfraConfigEnum.MAILER_SMTP_URL, + InfraConfigEnum.MAILER_ADDRESS_FROM, + ], +}; + /** * Load environment variables from the database and set them in the process * @@ -42,3 +65,42 @@ export function stopApp() { process.kill(process.pid, 'SIGTERM'); }, 5000); } + +/** + * Get the configured SSO providers + * @returns Array of configured SSO providers + */ +export function getConfiguredSSOProviders() { + const allowedAuthProviders: string[] = + process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(','); + let configuredAuthProviders: string[] = []; + + const addProviderIfConfigured = (provider) => { + const configParameters: string[] = AuthProviderConfigurations[provider]; + + const isConfigured = configParameters.every((configParameter) => { + return process.env[configParameter]; + }); + + if (isConfigured) configuredAuthProviders.push(provider); + }; + + allowedAuthProviders.forEach((provider) => addProviderIfConfigured(provider)); + + if (configuredAuthProviders.length === 0) { + throwErr(AUTH_PROVIDER_NOT_CONFIGURED); + } else if (allowedAuthProviders.length !== configuredAuthProviders.length) { + const unConfiguredAuthProviders = allowedAuthProviders.filter( + (provider) => { + return !configuredAuthProviders.includes(provider); + }, + ); + console.log( + `${unConfiguredAuthProviders.join( + ',', + )} SSO auth provider(s) are not configured properly. Do configure them from Admin Dashboard.`, + ); + } + + return configuredAuthProviders.join(','); +} 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 c1d419224..92be7e9dd 100644 --- a/packages/hoppscotch-backend/src/infra-config/infra-config.service.ts +++ b/packages/hoppscotch-backend/src/infra-config/infra-config.service.ts @@ -17,9 +17,9 @@ import { INFRA_CONFIG_UPDATE_FAILED, INFRA_CONFIG_SERVICE_NOT_CONFIGURED, } from 'src/errors'; -import { throwErr, validateEmail, validateSMTPUrl } from 'src/utils'; +import { throwErr, validateSMTPEmail, validateSMTPUrl } from 'src/utils'; import { ConfigService } from '@nestjs/config'; -import { ServiceStatus, stopApp } from './helper'; +import { ServiceStatus, getConfiguredSSOProviders, stopApp } from './helper'; import { EnableAndDisableSSOArgs, InfraConfigArgs } from './input-args'; import { AuthProvider } from 'src/auth/helper'; @@ -71,7 +71,7 @@ export class InfraConfigService implements OnModuleInit { }, { name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS, - value: process.env.VITE_ALLOWED_AUTH_PROVIDERS.toLocaleUpperCase(), + value: getConfiguredSSOProviders(), }, ]; @@ -130,6 +130,19 @@ export class InfraConfigService implements OnModuleInit { }; } + /** + * Get all the InfraConfigs as map + * @returns InfraConfig map + */ + async getInfraConfigsMap() { + const infraConfigs = await this.prisma.infraConfig.findMany(); + const infraConfigMap: Record = {}; + infraConfigs.forEach((config) => { + infraConfigMap[config.name] = config.value; + }); + return infraConfigMap; + } + /** * Update InfraConfig by name * @param name Name of the InfraConfig @@ -187,30 +200,24 @@ export class InfraConfigService implements OnModuleInit { /** * Check if the service is configured or not * @param service Service can be Auth Provider, Mailer, Audit Log etc. + * @param configMap Map of all the infra configs * @returns Either true or false */ - isServiceConfigured(service: AuthProvider) { + isServiceConfigured( + service: AuthProvider, + configMap: Record, + ) { switch (service) { case AuthProvider.GOOGLE: - return ( - this.configService.get('INFRA.GOOGLE_CLIENT_ID') && - this.configService.get('INFRA.GOOGLE_CLIENT_SECRET') - ); + return configMap.GOOGLE_CLIENT_ID && configMap.GOOGLE_CLIENT_SECRET; case AuthProvider.GITHUB: - return ( - this.configService.get('INFRA.GITHUB_CLIENT_ID') && - !this.configService.get('INFRA.GITHUB_CLIENT_SECRET') - ); + return configMap.GITHUB_CLIENT_ID && configMap.GITHUB_CLIENT_SECRET; case AuthProvider.MICROSOFT: return ( - this.configService.get('INFRA.MICROSOFT_CLIENT_ID') && - !this.configService.get('INFRA.MICROSOFT_CLIENT_SECRET') + configMap.MICROSOFT_CLIENT_ID && configMap.MICROSOFT_CLIENT_SECRET ); case AuthProvider.EMAIL: - return ( - this.configService.get('INFRA.MAILER_SMTP_URL') && - this.configService.get('INFRA.MAILER_ADDRESS_FROM') - ); + return configMap.MAILER_SMTP_URL && configMap.MAILER_ADDRESS_FROM; default: return false; } @@ -229,11 +236,11 @@ export class InfraConfigService implements OnModuleInit { let updatedAuthProviders = allowedAuthProviders; - for (let i = 0; i < providerInfo.length; i++) { - const { provider, status } = providerInfo[i]; + const infraConfigMap = await this.getInfraConfigsMap(); + providerInfo.forEach(({ provider, status }) => { if (status === ServiceStatus.ENABLE) { - const isConfigured = this.isServiceConfigured(provider); + const isConfigured = this.isServiceConfigured(provider, infraConfigMap); if (!isConfigured) { throwErr(INFRA_CONFIG_SERVICE_NOT_CONFIGURED); } @@ -243,7 +250,7 @@ export class InfraConfigService implements OnModuleInit { (p) => p !== provider, ); } - } + }); updatedAuthProviders = [...new Set(updatedAuthProviders)]; @@ -342,7 +349,7 @@ export class InfraConfigService implements OnModuleInit { if (!isValidUrl) return E.left(INFRA_CONFIG_INVALID_INPUT); break; case InfraConfigEnumForClient.MAILER_ADDRESS_FROM: - const isValidEmail = validateEmail(infraConfigs[i].value); + const isValidEmail = validateSMTPEmail(infraConfigs[i].value); if (!isValidEmail) return E.left(INFRA_CONFIG_INVALID_INPUT); break; case InfraConfigEnumForClient.GOOGLE_CLIENT_ID: diff --git a/packages/hoppscotch-backend/src/main.ts b/packages/hoppscotch-backend/src/main.ts index dae82a3a0..a32079c74 100644 --- a/packages/hoppscotch-backend/src/main.ts +++ b/packages/hoppscotch-backend/src/main.ts @@ -17,7 +17,8 @@ async function bootstrap() { console.log(`Port: ${configService.get('PORT')}`); checkEnvironmentAuthProvider( - configService.get('VITE_ALLOWED_AUTH_PROVIDERS'), + configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS') ?? + configService.get('VITE_ALLOWED_AUTH_PROVIDERS'), ); app.use( diff --git a/packages/hoppscotch-backend/src/utils.ts b/packages/hoppscotch-backend/src/utils.ts index cc74c30f2..76cf26ebc 100644 --- a/packages/hoppscotch-backend/src/utils.ts +++ b/packages/hoppscotch-backend/src/utils.ts @@ -131,6 +131,28 @@ export const validateEmail = (email: string) => { ).test(email); }; +// Regular expressions for supported address object formats by nodemailer +// check out for more info https://nodemailer.com/message/addresses +const emailRegex1 = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; +const emailRegex2 = + /^[\w\s]* <([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})>$/; +const emailRegex3 = + /^"[\w\s]+" <([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})>$/; + +/** + * Checks to see if the SMTP email is valid or not + * @param email + * @returns A Boolean depending on the format of the email + */ +export const validateSMTPEmail = (email: string) => { + // Check if the input matches any of the formats + return ( + emailRegex1.test(email) || + emailRegex2.test(email) || + emailRegex3.test(email) + ); +}; + /** * Checks to see if the URL is valid or not * @param url The URL to validate