Compare commits

...

15 Commits

Author SHA1 Message Date
mirarifhasan
f5a654b27a test: added test coverage for infra-config 2024-03-07 11:40:49 +05:30
mirarifhasan
c2c5cf25b1 test: fix failed test cases 2024-03-07 11:40:49 +05:30
mirarifhasan
f92c70e1ff feat: remove InfraConfigEnumForClient enum and add exclude const variable 2024-03-07 11:40:49 +05:30
mirarifhasan
f0adc5b2e4 chore: rebased and resolve conflicts 2024-03-07 11:40:49 +05:30
mirarifhasan
fdcf55552a fix: add return statement 2024-03-07 11:40:49 +05:30
mirarifhasan
b0b7df0a3e feat: removed unnecessary checks 2024-03-07 11:40:49 +05:30
mirarifhasan
01fd27f81a feat: infra config key check added instead of count check 2024-03-07 11:40:49 +05:30
mirarifhasan
2d7fb8e23a fix: pnpm issue 2024-03-07 11:40:49 +05:30
mirarifhasan
600e0eea76 fix: checks added for infraConfig table containing missing value or not 2024-03-07 11:40:49 +05:30
mirarifhasan
9e9907b4be feat: feedback implemented 2024-03-07 11:40:49 +05:30
Mir Arif Hasan
2ba79a043b fix: url regex update 2024-03-07 11:40:49 +05:30
Mir Arif Hasan
1be466efcd fix: code scanning issue 2024-03-07 11:40:49 +05:30
Mir Arif Hasan
0c13ca7dca feat: update configService in strategy file 2024-03-07 11:40:49 +05:30
Mir Arif Hasan
b6b6acd2fa fix: validate url function 2024-03-07 11:40:49 +05:30
Mir Arif Hasan
9c00d6238e feat: sso callback url and scope added in infra-config 2024-03-07 11:40:49 +05:30
14 changed files with 394 additions and 144 deletions

View File

@@ -32,7 +32,7 @@ import {
EnableAndDisableSSOArgs, EnableAndDisableSSOArgs,
InfraConfigArgs, InfraConfigArgs,
} from 'src/infra-config/input-args'; } 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'; import { ServiceStatus } from 'src/infra-config/helper';
@UseGuards(GqlThrottlerGuard) @UseGuards(GqlThrottlerGuard)
@@ -274,10 +274,10 @@ export class InfraResolver {
async infraConfigs( async infraConfigs(
@Args({ @Args({
name: 'configNames', name: 'configNames',
type: () => [InfraConfigEnumForClient], type: () => [InfraConfigEnum],
description: 'Configs to fetch', description: 'Configs to fetch',
}) })
names: InfraConfigEnumForClient[], names: InfraConfigEnum[],
) { ) {
const infraConfigs = await this.infraConfigService.getMany(names); const infraConfigs = await this.infraConfigService.getMany(names);
if (E.isLeft(infraConfigs)) throwErr(infraConfigs.left); if (E.isLeft(infraConfigs)) throwErr(infraConfigs.left);

View File

@@ -12,7 +12,10 @@ import { GithubStrategy } from './strategies/github.strategy';
import { MicrosoftStrategy } from './strategies/microsoft.strategy'; import { MicrosoftStrategy } from './strategies/microsoft.strategy';
import { AuthProvider, authProviderCheck } from './helper'; import { AuthProvider, authProviderCheck } from './helper';
import { ConfigModule, ConfigService } from '@nestjs/config'; 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'; import { InfraConfigModule } from 'src/infra-config/infra-config.module';
@Module({ @Module({
@@ -34,6 +37,11 @@ import { InfraConfigModule } from 'src/infra-config/infra-config.module';
}) })
export class AuthModule { export class AuthModule {
static async register() { static async register() {
const isInfraConfigPopulated = await isInfraConfigTablePopulated();
if (!isInfraConfigPopulated) {
return { module: AuthModule };
}
const env = await loadInfraConfiguration(); const env = await loadInfraConfiguration();
const allowedAuthProviders = env.INFRA.VITE_ALLOWED_AUTH_PROVIDERS; const allowedAuthProviders = env.INFRA.VITE_ALLOWED_AUTH_PROVIDERS;

View File

@@ -17,8 +17,8 @@ export class GithubStrategy extends PassportStrategy(Strategy) {
super({ super({
clientID: configService.get('INFRA.GITHUB_CLIENT_ID'), clientID: configService.get('INFRA.GITHUB_CLIENT_ID'),
clientSecret: configService.get('INFRA.GITHUB_CLIENT_SECRET'), clientSecret: configService.get('INFRA.GITHUB_CLIENT_SECRET'),
callbackURL: configService.get('GITHUB_CALLBACK_URL'), callbackURL: configService.get('INFRA.GITHUB_CALLBACK_URL'),
scope: [configService.get('GITHUB_SCOPE')], scope: [configService.get('INFRA.GITHUB_SCOPE')],
store: true, store: true,
}); });
} }

View File

@@ -17,8 +17,8 @@ export class GoogleStrategy extends PassportStrategy(Strategy) {
super({ super({
clientID: configService.get('INFRA.GOOGLE_CLIENT_ID'), clientID: configService.get('INFRA.GOOGLE_CLIENT_ID'),
clientSecret: configService.get('INFRA.GOOGLE_CLIENT_SECRET'), clientSecret: configService.get('INFRA.GOOGLE_CLIENT_SECRET'),
callbackURL: configService.get('GOOGLE_CALLBACK_URL'), callbackURL: configService.get('INFRA.GOOGLE_CALLBACK_URL'),
scope: configService.get('GOOGLE_SCOPE').split(','), scope: configService.get('INFRA.GOOGLE_SCOPE').split(','),
passReqToCallback: true, passReqToCallback: true,
store: true, store: true,
}); });

View File

@@ -17,9 +17,9 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy) {
super({ super({
clientID: configService.get('INFRA.MICROSOFT_CLIENT_ID'), clientID: configService.get('INFRA.MICROSOFT_CLIENT_ID'),
clientSecret: configService.get('INFRA.MICROSOFT_CLIENT_SECRET'), clientSecret: configService.get('INFRA.MICROSOFT_CLIENT_SECRET'),
callbackURL: configService.get('MICROSOFT_CALLBACK_URL'), callbackURL: configService.get('INFRA.MICROSOFT_CALLBACK_URL'),
scope: [configService.get('MICROSOFT_SCOPE')], scope: [configService.get('INFRA.MICROSOFT_SCOPE')],
tenant: configService.get('MICROSOFT_TENANT'), tenant: configService.get('INFRA.MICROSOFT_TENANT'),
store: true, store: true,
}); });
} }

View File

@@ -731,6 +731,13 @@ export const INFRA_CONFIG_INVALID_INPUT = 'infra_config/invalid_input' as const;
export const INFRA_CONFIG_SERVICE_NOT_CONFIGURED = export const INFRA_CONFIG_SERVICE_NOT_CONFIGURED =
'infra_config/service_not_configured' as const; '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 * Error message for when the database table does not exist
* (InfraConfigService) * (InfraConfigService)

View File

@@ -1,5 +1,8 @@
import { AuthProvider } from 'src/auth/helper'; 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 { PrismaService } from 'src/prisma/prisma.service';
import { InfraConfigEnum } from 'src/types/InfraConfig'; import { InfraConfigEnum } from 'src/types/InfraConfig';
import { throwErr } from 'src/utils'; import { throwErr } from 'src/utils';
@@ -14,14 +17,21 @@ const AuthProviderConfigurations = {
[AuthProvider.GOOGLE]: [ [AuthProvider.GOOGLE]: [
InfraConfigEnum.GOOGLE_CLIENT_ID, InfraConfigEnum.GOOGLE_CLIENT_ID,
InfraConfigEnum.GOOGLE_CLIENT_SECRET, InfraConfigEnum.GOOGLE_CLIENT_SECRET,
InfraConfigEnum.GOOGLE_CALLBACK_URL,
InfraConfigEnum.GOOGLE_SCOPE,
], ],
[AuthProvider.GITHUB]: [ [AuthProvider.GITHUB]: [
InfraConfigEnum.GITHUB_CLIENT_ID, InfraConfigEnum.GITHUB_CLIENT_ID,
InfraConfigEnum.GITHUB_CLIENT_SECRET, InfraConfigEnum.GITHUB_CLIENT_SECRET,
InfraConfigEnum.GITHUB_CALLBACK_URL,
InfraConfigEnum.GITHUB_SCOPE,
], ],
[AuthProvider.MICROSOFT]: [ [AuthProvider.MICROSOFT]: [
InfraConfigEnum.MICROSOFT_CLIENT_ID, InfraConfigEnum.MICROSOFT_CLIENT_ID,
InfraConfigEnum.MICROSOFT_CLIENT_SECRET, InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
InfraConfigEnum.MICROSOFT_CALLBACK_URL,
InfraConfigEnum.MICROSOFT_SCOPE,
InfraConfigEnum.MICROSOFT_TENANT,
], ],
[AuthProvider.EMAIL]: [ [AuthProvider.EMAIL]: [
InfraConfigEnum.MAILER_SMTP_URL, 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<boolean> {
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 * Stop the app after 5 seconds
* (Docker will re-start the app) * (Docker will re-start the app)

View File

@@ -5,7 +5,7 @@ import * as E from 'fp-ts/Either';
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'; import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
import { RESTAdminGuard } from 'src/admin/guards/rest-admin.guard'; import { RESTAdminGuard } from 'src/admin/guards/rest-admin.guard';
import { RESTError } from 'src/types/RESTError'; import { RESTError } from 'src/types/RESTError';
import { InfraConfigEnumForClient } from 'src/types/InfraConfig'; import { InfraConfigEnum } from 'src/types/InfraConfig';
import { throwHTTPErr } from 'src/utils'; import { throwHTTPErr } from 'src/utils';
@UseGuards(ThrottlerBehindProxyGuard) @UseGuards(ThrottlerBehindProxyGuard)
@@ -17,7 +17,7 @@ export class SiteController {
@UseGuards(JwtAuthGuard, RESTAdminGuard) @UseGuards(JwtAuthGuard, RESTAdminGuard)
async fetchSetupInfo() { async fetchSetupInfo() {
const status = await this.infraConfigService.get( const status = await this.infraConfigService.get(
InfraConfigEnumForClient.IS_FIRST_TIME_INFRA_SETUP, InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
); );
if (E.isLeft(status)) if (E.isLeft(status))
@@ -32,7 +32,7 @@ export class SiteController {
@UseGuards(JwtAuthGuard, RESTAdminGuard) @UseGuards(JwtAuthGuard, RESTAdminGuard)
async setSetupAsComplete() { async setSetupAsComplete() {
const res = await this.infraConfigService.update( const res = await this.infraConfigService.update(
InfraConfigEnumForClient.IS_FIRST_TIME_INFRA_SETUP, InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
false.toString(), false.toString(),
false, false,
); );

View File

@@ -1,6 +1,6 @@
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql'; import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { AuthProvider } from 'src/auth/helper'; import { AuthProvider } from 'src/auth/helper';
import { InfraConfigEnumForClient } from 'src/types/InfraConfig'; import { InfraConfigEnum } from 'src/types/InfraConfig';
import { ServiceStatus } from './helper'; import { ServiceStatus } from './helper';
@ObjectType() @ObjectType()
@@ -8,7 +8,7 @@ export class InfraConfig {
@Field({ @Field({
description: 'Infra Config Name', description: 'Infra Config Name',
}) })
name: InfraConfigEnumForClient; name: InfraConfigEnum;
@Field({ @Field({
description: 'Infra Config Value', description: 'Infra Config Value',
@@ -16,7 +16,7 @@ export class InfraConfig {
value: string; value: string;
} }
registerEnumType(InfraConfigEnumForClient, { registerEnumType(InfraConfigEnum, {
name: 'InfraConfigEnum', name: 'InfraConfigEnum',
}); });

View File

@@ -1,13 +1,16 @@
import { mockDeep, mockReset } from 'jest-mock-extended'; import { mockDeep, mockReset } from 'jest-mock-extended';
import { PrismaService } from 'src/prisma/prisma.service'; import { PrismaService } from 'src/prisma/prisma.service';
import { InfraConfigService } from './infra-config.service'; import { InfraConfigService } from './infra-config.service';
import { InfraConfigEnum } from 'src/types/InfraConfig';
import { import {
InfraConfigEnum, INFRA_CONFIG_NOT_FOUND,
InfraConfigEnumForClient, INFRA_CONFIG_OPERATION_NOT_ALLOWED,
} from 'src/types/InfraConfig'; INFRA_CONFIG_UPDATE_FAILED,
import { INFRA_CONFIG_NOT_FOUND, INFRA_CONFIG_UPDATE_FAILED } from 'src/errors'; } from 'src/errors';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import * as helper from './helper'; import * as helper from './helper';
import { InfraConfig as dbInfraConfig } from '@prisma/client';
import { InfraConfig } from './infra-config.model';
const mockPrisma = mockDeep<PrismaService>(); const mockPrisma = mockDeep<PrismaService>();
const mockConfigService = mockDeep<ConfigService>(); const mockConfigService = mockDeep<ConfigService>();
@@ -19,12 +22,82 @@ const infraConfigService = new InfraConfigService(
mockConfigService, 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(() => { beforeEach(() => {
mockReset(mockPrisma); mockReset(mockPrisma);
}); });
describe('InfraConfigService', () => { describe('InfraConfigService', () => {
describe('update', () => { 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 () => { it('should update the infra config', async () => {
const name = InfraConfigEnum.GOOGLE_CLIENT_ID; const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
const value = 'true'; const value = 'true';
@@ -71,7 +144,7 @@ describe('InfraConfigService', () => {
describe('get', () => { describe('get', () => {
it('should get the infra config', async () => { it('should get the infra config', async () => {
const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID; const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
const value = 'true'; const value = 'true';
mockPrisma.infraConfig.findUniqueOrThrow.mockResolvedValueOnce({ mockPrisma.infraConfig.findUniqueOrThrow.mockResolvedValueOnce({
@@ -87,7 +160,7 @@ describe('InfraConfigService', () => {
}); });
it('should pass correct params to prisma findUnique', async () => { 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); await infraConfigService.get(name);
@@ -98,7 +171,7 @@ describe('InfraConfigService', () => {
}); });
it('should throw an error if the infra config does not exist', async () => { 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'); mockPrisma.infraConfig.findUniqueOrThrow.mockRejectedValueOnce('null');
@@ -106,4 +179,45 @@ describe('InfraConfigService', () => {
expect(result).toEqualLeft(INFRA_CONFIG_NOT_FOUND); 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)),
);
});
});
}); });

View File

@@ -3,28 +3,25 @@ import { InfraConfig } from './infra-config.model';
import { PrismaService } from 'src/prisma/prisma.service'; import { PrismaService } from 'src/prisma/prisma.service';
import { InfraConfig as DBInfraConfig } from '@prisma/client'; import { InfraConfig as DBInfraConfig } from '@prisma/client';
import * as E from 'fp-ts/Either'; import * as E from 'fp-ts/Either';
import { import { InfraConfigEnum } from 'src/types/InfraConfig';
InfraConfigEnum,
InfraConfigEnumForClient,
} from 'src/types/InfraConfig';
import { import {
AUTH_PROVIDER_NOT_SPECIFIED, AUTH_PROVIDER_NOT_SPECIFIED,
DATABASE_TABLE_NOT_EXIST, DATABASE_TABLE_NOT_EXIST,
INFRA_CONFIG_INVALID_INPUT, INFRA_CONFIG_INVALID_INPUT,
INFRA_CONFIG_NOT_FOUND, INFRA_CONFIG_NOT_FOUND,
INFRA_CONFIG_NOT_LISTED,
INFRA_CONFIG_RESET_FAILED, INFRA_CONFIG_RESET_FAILED,
INFRA_CONFIG_UPDATE_FAILED, INFRA_CONFIG_UPDATE_FAILED,
INFRA_CONFIG_SERVICE_NOT_CONFIGURED, INFRA_CONFIG_SERVICE_NOT_CONFIGURED,
INFRA_CONFIG_OPERATION_NOT_ALLOWED,
} from 'src/errors'; } from 'src/errors';
import { throwErr, validateSMTPEmail, validateSMTPUrl } from 'src/utils';
import { ConfigService } from '@nestjs/config';
import { import {
ServiceStatus, throwErr,
generateAnalyticsUserId, validateSMTPEmail,
getConfiguredSSOProviders, validateSMTPUrl,
stopApp, validateUrl,
} from './helper'; } from 'src/utils';
import { ConfigService } from '@nestjs/config';
import { ServiceStatus, getDefaultInfraConfigs, stopApp } from './helper';
import { EnableAndDisableSSOArgs, InfraConfigArgs } from './input-args'; import { EnableAndDisableSSOArgs, InfraConfigArgs } from './input-args';
import { AuthProvider } from 'src/auth/helper'; import { AuthProvider } from 'src/auth/helper';
@@ -35,66 +32,22 @@ export class InfraConfigService implements OnModuleInit {
private readonly configService: ConfigService, private readonly configService: ConfigService,
) {} ) {}
async onModuleInit() { // Following fields are not updatable by `infraConfigs` Mutation. Use dedicated mutations for these fields instead.
await this.initializeInfraConfigTable(); EXCLUDE_FROM_UPDATE_CONFIGS = [
} InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
async getDefaultInfraConfigs(): Promise< InfraConfigEnum.ANALYTICS_USER_ID,
{ name: InfraConfigEnum; value: string }[] InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
> { ];
// Prepare rows for 'infra_config' table with default values (from .env) for each 'name' // Following fields can not be fetched by `infraConfigs` Query. Use dedicated queries for these fields instead.
const infraConfigDefaultObjs: { name: InfraConfigEnum; value: string }[] = [ EXCLUDE_FROM_FETCH_CONFIGS = [
{ InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
name: InfraConfigEnum.MAILER_SMTP_URL, InfraConfigEnum.ANALYTICS_USER_ID,
value: process.env.MAILER_SMTP_URL, InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
},
{
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; async onModuleInit() {
await this.initializeInfraConfigTable();
} }
/** /**
@@ -103,16 +56,8 @@ export class InfraConfigService implements OnModuleInit {
*/ */
async initializeInfraConfigTable() { async initializeInfraConfigTable() {
try { 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 // Fetch the default values (value in .env) for configs to be saved in 'infra_config' table
const infraConfigDefaultObjs = await this.getDefaultInfraConfigs(); const infraConfigDefaultObjs = await getDefaultInfraConfigs();
// Check if all the 'names' are listed in the default values
if (enumValues.length !== infraConfigDefaultObjs.length) {
throw new Error(INFRA_CONFIG_NOT_LISTED);
}
// Eliminate the rows (from 'infraConfigDefaultObjs') that are already present in the database table // Eliminate the rows (from 'infraConfigDefaultObjs') that are already present in the database table
const dbInfraConfigs = await this.prisma.infraConfig.findMany(); 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 * @param restartEnabled If true, restart the app after updating the InfraConfig
* @returns InfraConfig model * @returns InfraConfig model
*/ */
async update( async update(name: InfraConfigEnum, value: string, restartEnabled = false) {
name: InfraConfigEnumForClient | InfraConfigEnum,
value: string,
restartEnabled = false,
) {
const isValidate = this.validateEnvValues([{ name, value }]); const isValidate = this.validateEnvValues([{ name, value }]);
if (E.isLeft(isValidate)) return E.left(isValidate.left); if (E.isLeft(isValidate)) return E.left(isValidate.left);
@@ -197,6 +138,11 @@ export class InfraConfigService implements OnModuleInit {
* @returns InfraConfig model * @returns InfraConfig model
*/ */
async updateMany(infraConfigs: InfraConfigArgs[]) { 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); const isValidate = this.validateEnvValues(infraConfigs);
if (E.isLeft(isValidate)) return E.left(isValidate.left); if (E.isLeft(isValidate)) return E.left(isValidate.left);
@@ -230,12 +176,26 @@ export class InfraConfigService implements OnModuleInit {
) { ) {
switch (service) { switch (service) {
case AuthProvider.GOOGLE: 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: 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: case AuthProvider.MICROSOFT:
return ( 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: case AuthProvider.EMAIL:
return configMap.MAILER_SMTP_URL && configMap.MAILER_ADDRESS_FROM; return configMap.MAILER_SMTP_URL && configMap.MAILER_ADDRESS_FROM;
@@ -310,7 +270,7 @@ export class InfraConfigService implements OnModuleInit {
* @param name Name of the InfraConfig * @param name Name of the InfraConfig
* @returns InfraConfig model * @returns InfraConfig model
*/ */
async get(name: InfraConfigEnumForClient) { async get(name: InfraConfigEnum) {
try { try {
const infraConfig = await this.prisma.infraConfig.findUniqueOrThrow({ const infraConfig = await this.prisma.infraConfig.findUniqueOrThrow({
where: { name }, where: { name },
@@ -327,7 +287,15 @@ export class InfraConfigService implements OnModuleInit {
* @param names Names of the InfraConfigs * @param names Names of the InfraConfigs
* @returns InfraConfig model * @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 { try {
const infraConfigs = await this.prisma.infraConfig.findMany({ const infraConfigs = await this.prisma.infraConfig.findMany({
where: { name: { in: names } }, where: { name: { in: names } },
@@ -354,7 +322,7 @@ export class InfraConfigService implements OnModuleInit {
*/ */
async reset() { async reset() {
try { try {
const infraConfigDefaultObjs = await this.getDefaultInfraConfigs(); const infraConfigDefaultObjs = await getDefaultInfraConfigs();
await this.prisma.infraConfig.deleteMany({ await this.prisma.infraConfig.deleteMany({
where: { name: { in: infraConfigDefaultObjs.map((p) => p.name) } }, where: { name: { in: infraConfigDefaultObjs.map((p) => p.name) } },
@@ -387,36 +355,60 @@ export class InfraConfigService implements OnModuleInit {
*/ */
validateEnvValues( validateEnvValues(
infraConfigs: { infraConfigs: {
name: InfraConfigEnumForClient | InfraConfigEnum; name: InfraConfigEnum;
value: string; value: string;
}[], }[],
) { ) {
for (let i = 0; i < infraConfigs.length; i++) { for (let i = 0; i < infraConfigs.length; i++) {
switch (infraConfigs[i].name) { switch (infraConfigs[i].name) {
case InfraConfigEnumForClient.MAILER_SMTP_URL: case InfraConfigEnum.MAILER_SMTP_URL:
const isValidUrl = validateSMTPUrl(infraConfigs[i].value); const isValidUrl = validateSMTPUrl(infraConfigs[i].value);
if (!isValidUrl) return E.left(INFRA_CONFIG_INVALID_INPUT); if (!isValidUrl) return E.left(INFRA_CONFIG_INVALID_INPUT);
break; break;
case InfraConfigEnumForClient.MAILER_ADDRESS_FROM: case InfraConfigEnum.MAILER_ADDRESS_FROM:
const isValidEmail = validateSMTPEmail(infraConfigs[i].value); const isValidEmail = validateSMTPEmail(infraConfigs[i].value);
if (!isValidEmail) return E.left(INFRA_CONFIG_INVALID_INPUT); if (!isValidEmail) return E.left(INFRA_CONFIG_INVALID_INPUT);
break; break;
case InfraConfigEnumForClient.GOOGLE_CLIENT_ID: case InfraConfigEnum.GOOGLE_CLIENT_ID:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT); if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break; break;
case InfraConfigEnumForClient.GOOGLE_CLIENT_SECRET: case InfraConfigEnum.GOOGLE_CLIENT_SECRET:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT); if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break; 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); if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break; break;
case InfraConfigEnumForClient.GITHUB_CLIENT_SECRET: case InfraConfigEnum.GITHUB_CLIENT_ID:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT); if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break; break;
case InfraConfigEnumForClient.MICROSOFT_CLIENT_ID: case InfraConfigEnum.GITHUB_CLIENT_SECRET:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT); if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break; 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); if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break; break;
default: default:

View File

@@ -1,14 +1,14 @@
import { Field, InputType } from '@nestjs/graphql'; import { Field, InputType } from '@nestjs/graphql';
import { InfraConfigEnumForClient } from 'src/types/InfraConfig'; import { InfraConfigEnum } from 'src/types/InfraConfig';
import { ServiceStatus } from './helper'; import { ServiceStatus } from './helper';
import { AuthProvider } from 'src/auth/helper'; import { AuthProvider } from 'src/auth/helper';
@InputType() @InputType()
export class InfraConfigArgs { export class InfraConfigArgs {
@Field(() => InfraConfigEnumForClient, { @Field(() => InfraConfigEnum, {
description: 'Infra Config Name', description: 'Infra Config Name',
}) })
name: InfraConfigEnumForClient; name: InfraConfigEnum;
@Field({ @Field({
description: 'Infra Config Value', description: 'Infra Config Value',

View File

@@ -4,12 +4,19 @@ export enum InfraConfigEnum {
GOOGLE_CLIENT_ID = 'GOOGLE_CLIENT_ID', GOOGLE_CLIENT_ID = 'GOOGLE_CLIENT_ID',
GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET', GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET',
GOOGLE_CALLBACK_URL = 'GOOGLE_CALLBACK_URL',
GOOGLE_SCOPE = 'GOOGLE_SCOPE',
GITHUB_CLIENT_ID = 'GITHUB_CLIENT_ID', GITHUB_CLIENT_ID = 'GITHUB_CLIENT_ID',
GITHUB_CLIENT_SECRET = 'GITHUB_CLIENT_SECRET', GITHUB_CLIENT_SECRET = 'GITHUB_CLIENT_SECRET',
GITHUB_CALLBACK_URL = 'GITHUB_CALLBACK_URL',
GITHUB_SCOPE = 'GITHUB_SCOPE',
MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID', MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID',
MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET', 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', VITE_ALLOWED_AUTH_PROVIDERS = 'VITE_ALLOWED_AUTH_PROVIDERS',
@@ -17,20 +24,3 @@ export enum InfraConfigEnum {
ANALYTICS_USER_ID = 'ANALYTICS_USER_ID', ANALYTICS_USER_ID = 'ANALYTICS_USER_ID',
IS_FIRST_TIME_INFRA_SETUP = 'IS_FIRST_TIME_INFRA_SETUP', 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',
}

View File

@@ -183,6 +183,16 @@ export const validateSMTPUrl = (url: string) => {
return false; 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 * String to JSON parser
* @param {str} str The string to parse * @param {str} str The string to parse