Compare commits

...

29 Commits

Author SHA1 Message Date
Mir Arif Hasan
eec5b8f9c8 chore: app shutdown way changed 2023-12-12 16:40:06 +06:00
Mir Arif Hasan
99d369fc79 chore: renamed an enum 2023-12-12 16:40:06 +06:00
Mir Arif Hasan
7da5063a8c chore: feedback resolve 2023-12-12 16:40:06 +06:00
Mir Arif Hasan
1957556b5a test: fix test case 2023-12-12 16:40:06 +06:00
Mir Arif Hasan
84fdcaf840 fix: add validation checks for MAILER_ADDRESS_FROM 2023-12-12 16:40:06 +06:00
Andrew Bastin
1f684d47e2 chore: update lockfile 2023-12-12 16:40:06 +06:00
Mir Arif Hasan
59dcd57d9c fix: uppercase issue of VITE_ALLOWED_AUTH_PROVIDERS 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
fc7817780d chore: update code comments 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
0f7aa5e84d feat: feedback resolve 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
85dea99452 feat: get api added for fetch provider list 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
94ca981144 fix: validateSMTPUrl check 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
8035d1d592 fix: allowedAuthProviders and enableAndDisableSSO 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
02dcc018fa fix: pnpm i without db connection 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
11e2e18aa3 fix: all sso disabling is handled 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
97d71e5032 chore: smtp url validation added 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
180b9d776f fix: mailer module init issue 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
c4a9038579 feat: add query infra-configs 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
8df8b056d9 feat: added mutations and query 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
01ccb321f1 fix: mailer module init with right env value 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
9f62ed6c90 fix: config service precedence 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
ef8412544a feat: removed saml stuffs 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
1d05a76b59 chore: remove saml stuff 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
6157848f56 feat: utilise ConfigService in util functions 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
0a10f7c654 feat: update infra configs mutation added 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
4e188f3c11 test: fix test case failure 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
c3522025c8 feat: infra config module add with get-update-reset functionality 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
74cec0365b test: fix all broken test case 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
5bee3471c5 feat: nestjs config package added 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
49c2ee0d38 feat: restart cmd added in aio service 2023-12-12 16:38:50 +06:00
37 changed files with 1735 additions and 631 deletions

View File

@@ -66,6 +66,7 @@ services:
# The service that spins up all 3 services at once in one container # The service that spins up all 3 services at once in one container
hoppscotch-aio: hoppscotch-aio:
container_name: hoppscotch-aio container_name: hoppscotch-aio
restart: unless-stopped
build: build:
dockerfile: prod.Dockerfile dockerfile: prod.Dockerfile
context: . context: .

View File

@@ -28,6 +28,7 @@
"@nestjs-modules/mailer": "^1.9.1", "@nestjs-modules/mailer": "^1.9.1",
"@nestjs/apollo": "^12.0.9", "@nestjs/apollo": "^12.0.9",
"@nestjs/common": "^10.2.6", "@nestjs/common": "^10.2.6",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.2.6", "@nestjs/core": "^10.2.6",
"@nestjs/graphql": "^12.0.9", "@nestjs/graphql": "^12.0.9",
"@nestjs/jwt": "^10.1.1", "@nestjs/jwt": "^10.1.1",

View File

@@ -0,0 +1,14 @@
-- CreateTable
CREATE TABLE "InfraConfig" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"value" TEXT,
"active" BOOLEAN NOT NULL DEFAULT true,
"createdOn" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedOn" TIMESTAMP(3) NOT NULL,
CONSTRAINT "InfraConfig_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "InfraConfig_name_key" ON "InfraConfig"("name");

View File

@@ -209,3 +209,12 @@ enum TeamMemberRole {
VIEWER VIEWER
EDITOR EDITOR
} }
model InfraConfig {
id String @id @default(cuid())
name String @unique
value String?
active Boolean @default(true) // Use case: Let's say, Admin wants to disable Google SSO, but doesn't want to delete the config
createdOn DateTime @default(now()) @db.Timestamp(3)
updatedOn DateTime @updatedAt @db.Timestamp(3)
}

View File

@@ -4,7 +4,6 @@ import { AdminService } from './admin.service';
import { PrismaModule } from '../prisma/prisma.module'; import { PrismaModule } from '../prisma/prisma.module';
import { PubSubModule } from '../pubsub/pubsub.module'; import { PubSubModule } from '../pubsub/pubsub.module';
import { UserModule } from '../user/user.module'; import { UserModule } from '../user/user.module';
import { MailerModule } from '../mailer/mailer.module';
import { TeamModule } from '../team/team.module'; import { TeamModule } from '../team/team.module';
import { TeamInvitationModule } from '../team-invitation/team-invitation.module'; import { TeamInvitationModule } from '../team-invitation/team-invitation.module';
import { TeamEnvironmentsModule } from '../team-environments/team-environments.module'; import { TeamEnvironmentsModule } from '../team-environments/team-environments.module';
@@ -12,19 +11,20 @@ import { TeamCollectionModule } from '../team-collection/team-collection.module'
import { TeamRequestModule } from '../team-request/team-request.module'; import { TeamRequestModule } from '../team-request/team-request.module';
import { InfraResolver } from './infra.resolver'; import { InfraResolver } from './infra.resolver';
import { ShortcodeModule } from 'src/shortcode/shortcode.module'; import { ShortcodeModule } from 'src/shortcode/shortcode.module';
import { InfraConfigModule } from 'src/infra-config/infra-config.module';
@Module({ @Module({
imports: [ imports: [
PrismaModule, PrismaModule,
PubSubModule, PubSubModule,
UserModule, UserModule,
MailerModule,
TeamModule, TeamModule,
TeamInvitationModule, TeamInvitationModule,
TeamEnvironmentsModule, TeamEnvironmentsModule,
TeamCollectionModule, TeamCollectionModule,
TeamRequestModule, TeamRequestModule,
ShortcodeModule, ShortcodeModule,
InfraConfigModule,
], ],
providers: [InfraResolver, AdminResolver, AdminService], providers: [InfraResolver, AdminResolver, AdminService],
exports: [AdminService], exports: [AdminService],

View File

@@ -16,6 +16,7 @@ import {
USER_ALREADY_INVITED, USER_ALREADY_INVITED,
} from '../errors'; } from '../errors';
import { ShortcodeService } from 'src/shortcode/shortcode.service'; import { ShortcodeService } from 'src/shortcode/shortcode.service';
import { ConfigService } from '@nestjs/config';
const mockPrisma = mockDeep<PrismaService>(); const mockPrisma = mockDeep<PrismaService>();
const mockPubSub = mockDeep<PubSubService>(); const mockPubSub = mockDeep<PubSubService>();
@@ -27,6 +28,7 @@ const mockTeamInvitationService = mockDeep<TeamInvitationService>();
const mockTeamCollectionService = mockDeep<TeamCollectionService>(); const mockTeamCollectionService = mockDeep<TeamCollectionService>();
const mockMailerService = mockDeep<MailerService>(); const mockMailerService = mockDeep<MailerService>();
const mockShortcodeService = mockDeep<ShortcodeService>(); const mockShortcodeService = mockDeep<ShortcodeService>();
const mockConfigService = mockDeep<ConfigService>();
const adminService = new AdminService( const adminService = new AdminService(
mockUserService, mockUserService,
@@ -39,6 +41,7 @@ const adminService = new AdminService(
mockPrisma as any, mockPrisma as any,
mockMailerService, mockMailerService,
mockShortcodeService, mockShortcodeService,
mockConfigService,
); );
const invitedUsers: InvitedUsers[] = [ const invitedUsers: InvitedUsers[] = [

View File

@@ -25,6 +25,7 @@ import { TeamEnvironmentsService } from '../team-environments/team-environments.
import { TeamInvitationService } from '../team-invitation/team-invitation.service'; import { TeamInvitationService } from '../team-invitation/team-invitation.service';
import { TeamMemberRole } from '../team/team.model'; import { TeamMemberRole } from '../team/team.model';
import { ShortcodeService } from 'src/shortcode/shortcode.service'; import { ShortcodeService } from 'src/shortcode/shortcode.service';
import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class AdminService { export class AdminService {
@@ -39,6 +40,7 @@ export class AdminService {
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly mailerService: MailerService, private readonly mailerService: MailerService,
private readonly shortcodeService: ShortcodeService, private readonly shortcodeService: ShortcodeService,
private readonly configService: ConfigService,
) {} ) {}
/** /**
@@ -79,7 +81,7 @@ export class AdminService {
template: 'user-invitation', template: 'user-invitation',
variables: { variables: {
inviteeEmail: inviteeEmail, inviteeEmail: inviteeEmail,
magicLink: `${process.env.VITE_BASE_URL}`, magicLink: `${this.configService.get('VITE_BASE_URL')}`,
}, },
}); });
} catch (e) { } catch (e) {

View File

@@ -1,5 +1,12 @@
import { UseGuards } from '@nestjs/common'; import { UseGuards } from '@nestjs/common';
import { Args, ID, Query, ResolveField, Resolver } from '@nestjs/graphql'; import {
Args,
ID,
Mutation,
Query,
ResolveField,
Resolver,
} from '@nestjs/graphql';
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard'; import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
import { Infra } from './infra.model'; import { Infra } from './infra.model';
import { AdminService } from './admin.service'; import { AdminService } from './admin.service';
@@ -16,11 +23,21 @@ import { Team } from 'src/team/team.model';
import { TeamInvitation } from 'src/team-invitation/team-invitation.model'; import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
import { GqlAdmin } from './decorators/gql-admin.decorator'; import { GqlAdmin } from './decorators/gql-admin.decorator';
import { ShortcodeWithUserEmail } from 'src/shortcode/shortcode.model'; import { ShortcodeWithUserEmail } from 'src/shortcode/shortcode.model';
import { InfraConfig } from 'src/infra-config/infra-config.model';
import { InfraConfigService } from 'src/infra-config/infra-config.service';
import {
EnableAndDisableSSOArgs,
InfraConfigArgs,
} from 'src/infra-config/input-args';
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
@UseGuards(GqlThrottlerGuard) @UseGuards(GqlThrottlerGuard)
@Resolver(() => Infra) @Resolver(() => Infra)
export class InfraResolver { export class InfraResolver {
constructor(private adminService: AdminService) {} constructor(
private adminService: AdminService,
private infraConfigService: InfraConfigService,
) {}
@Query(() => Infra, { @Query(() => Infra, {
description: 'Fetch details of the Infrastructure', description: 'Fetch details of the Infrastructure',
@@ -222,4 +239,76 @@ export class InfraResolver {
userEmail, userEmail,
); );
} }
@Query(() => [InfraConfig], {
description: 'Retrieve configuration details for the instance',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async infraConfigs(
@Args({
name: 'configNames',
type: () => [InfraConfigEnumForClient],
description: 'Configs to fetch',
})
names: InfraConfigEnumForClient[],
) {
const infraConfigs = await this.infraConfigService.getMany(names);
if (E.isLeft(infraConfigs)) throwErr(infraConfigs.left);
return infraConfigs.right;
}
@Query(() => [String], {
description: 'Allowed Auth Provider list',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
allowedAuthProviders() {
return this.infraConfigService.getAllowedAuthProviders();
}
/* Mutations */
@Mutation(() => [InfraConfig], {
description: 'Update Infra Configs',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async updateInfraConfigs(
@Args({
name: 'infraConfigs',
type: () => [InfraConfigArgs],
description: 'InfraConfigs to update',
})
infraConfigs: InfraConfigArgs[],
) {
const updatedRes = await this.infraConfigService.updateMany(infraConfigs);
if (E.isLeft(updatedRes)) throwErr(updatedRes.left);
return updatedRes.right;
}
@Mutation(() => Boolean, {
description: 'Reset Infra Configs with default values (.env)',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async resetInfraConfigs() {
const resetRes = await this.infraConfigService.reset();
if (E.isLeft(resetRes)) throwErr(resetRes.left);
return true;
}
@Mutation(() => Boolean, {
description: 'Enable or Disable SSO for login/signup',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async enableAndDisableSSO(
@Args({
name: 'providerInfo',
type: () => [EnableAndDisableSSOArgs],
description: 'SSO provider and status',
})
providerInfo: EnableAndDisableSSOArgs[],
) {
const isUpdated = await this.infraConfigService.enableAndDisableSSO(providerInfo);
if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
return true;
}
} }

View File

@@ -20,51 +20,69 @@ import { ShortcodeModule } from './shortcode/shortcode.module';
import { COOKIES_NOT_FOUND } from './errors'; import { COOKIES_NOT_FOUND } from './errors';
import { ThrottlerModule } from '@nestjs/throttler'; import { ThrottlerModule } from '@nestjs/throttler';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { InfraConfigModule } from './infra-config/infra-config.module';
import { loadInfraConfiguration } from './infra-config/helper';
import { MailerModule } from './mailer/mailer.module';
@Module({ @Module({
imports: [ imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({ ConfigModule.forRoot({
buildSchemaOptions: { isGlobal: true,
numberScalarMode: 'integer', load: [async () => loadInfraConfiguration()],
},
playground: process.env.PRODUCTION !== 'true',
autoSchemaFile: true,
installSubscriptionHandlers: true,
subscriptions: {
'subscriptions-transport-ws': {
path: '/graphql',
onConnect: (_, websocket) => {
try {
const cookies = subscriptionContextCookieParser(
websocket.upgradeReq.headers.cookie,
);
return {
headers: { ...websocket?.upgradeReq?.headers, cookies },
};
} catch (error) {
throw new HttpException(COOKIES_NOT_FOUND, 400, {
cause: new Error(COOKIES_NOT_FOUND),
});
}
},
},
},
context: ({ req, res, connection }) => ({
req,
res,
connection,
}),
driver: ApolloDriver,
}), }),
ThrottlerModule.forRoot([ GraphQLModule.forRootAsync<ApolloDriverConfig>({
{ driver: ApolloDriver,
ttl: +process.env.RATE_LIMIT_TTL, imports: [ConfigModule],
limit: +process.env.RATE_LIMIT_MAX, inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
return {
buildSchemaOptions: {
numberScalarMode: 'integer',
},
playground: configService.get('PRODUCTION') !== 'true',
autoSchemaFile: true,
installSubscriptionHandlers: true,
subscriptions: {
'subscriptions-transport-ws': {
path: '/graphql',
onConnect: (_, websocket) => {
try {
const cookies = subscriptionContextCookieParser(
websocket.upgradeReq.headers.cookie,
);
return {
headers: { ...websocket?.upgradeReq?.headers, cookies },
};
} catch (error) {
throw new HttpException(COOKIES_NOT_FOUND, 400, {
cause: new Error(COOKIES_NOT_FOUND),
});
}
},
},
},
context: ({ req, res, connection }) => ({
req,
res,
connection,
}),
};
}, },
]), }),
ThrottlerModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => [
{
ttl: +configService.get('RATE_LIMIT_TTL'),
limit: +configService.get('RATE_LIMIT_MAX'),
},
],
}),
MailerModule.register(),
UserModule, UserModule,
AuthModule, AuthModule.register(),
AdminModule, AdminModule,
UserSettingsModule, UserSettingsModule,
UserEnvironmentsModule, UserEnvironmentsModule,
@@ -77,6 +95,7 @@ import { AppController } from './app.controller';
TeamInvitationModule, TeamInvitationModule,
UserCollectionModule, UserCollectionModule,
ShortcodeModule, ShortcodeModule,
InfraConfigModule,
], ],
providers: [GQLComplexityPlugin], providers: [GQLComplexityPlugin],
controllers: [AppController], controllers: [AppController],

View File

@@ -2,7 +2,6 @@ import {
Body, Body,
Controller, Controller,
Get, Get,
InternalServerErrorException,
Post, Post,
Query, Query,
Request, Request,
@@ -31,11 +30,21 @@ import { MicrosoftSSOGuard } from './guards/microsoft-sso-.guard';
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard'; import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
import { SkipThrottle } from '@nestjs/throttler'; import { SkipThrottle } from '@nestjs/throttler';
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors'; import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
import { ConfigService } from '@nestjs/config';
@UseGuards(ThrottlerBehindProxyGuard) @UseGuards(ThrottlerBehindProxyGuard)
@Controller({ path: 'auth', version: '1' }) @Controller({ path: 'auth', version: '1' })
export class AuthController { export class AuthController {
constructor(private authService: AuthService) {} constructor(
private authService: AuthService,
private configService: ConfigService,
) {}
@Get('providers')
async getAuthProviders() {
const providers = await this.authService.getAuthProviders();
return { providers };
}
/** /**
** Route to initiate magic-link auth for a users email ** Route to initiate magic-link auth for a users email
@@ -45,8 +54,14 @@ export class AuthController {
@Body() authData: SignInMagicDto, @Body() authData: SignInMagicDto,
@Query('origin') origin: string, @Query('origin') origin: string,
) { ) {
if (!authProviderCheck(AuthProvider.EMAIL)) if (
!authProviderCheck(
AuthProvider.EMAIL,
this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'),
)
) {
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 }); throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
}
const deviceIdToken = await this.authService.signInMagicLink( const deviceIdToken = await this.authService.signInMagicLink(
authData.email, authData.email,

View File

@@ -2,7 +2,6 @@ import { Module } from '@nestjs/common';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { AuthController } from './auth.controller'; import { AuthController } from './auth.controller';
import { UserModule } from 'src/user/user.module'; import { UserModule } from 'src/user/user.module';
import { MailerModule } from 'src/mailer/mailer.module';
import { PrismaModule } from 'src/prisma/prisma.module'; import { PrismaModule } from 'src/prisma/prisma.module';
import { PassportModule } from '@nestjs/passport'; import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt'; import { JwtModule } from '@nestjs/jwt';
@@ -12,25 +11,47 @@ import { GoogleStrategy } from './strategies/google.strategy';
import { GithubStrategy } from './strategies/github.strategy'; 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 { loadInfraConfiguration } from 'src/infra-config/helper';
import { InfraConfigModule } from 'src/infra-config/infra-config.module';
@Module({ @Module({
imports: [ imports: [
PrismaModule, PrismaModule,
UserModule, UserModule,
MailerModule,
PassportModule, PassportModule,
JwtModule.register({ JwtModule.registerAsync({
secret: process.env.JWT_SECRET, imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
secret: configService.get('JWT_SECRET'),
}),
}), }),
InfraConfigModule,
], ],
providers: [ providers: [AuthService, JwtStrategy, RTJwtStrategy],
AuthService,
JwtStrategy,
RTJwtStrategy,
...(authProviderCheck(AuthProvider.GOOGLE) ? [GoogleStrategy] : []),
...(authProviderCheck(AuthProvider.GITHUB) ? [GithubStrategy] : []),
...(authProviderCheck(AuthProvider.MICROSOFT) ? [MicrosoftStrategy] : []),
],
controllers: [AuthController], controllers: [AuthController],
}) })
export class AuthModule {} export class AuthModule {
static async register() {
const env = await loadInfraConfiguration();
const allowedAuthProviders = env.INFRA.VITE_ALLOWED_AUTH_PROVIDERS;
const providers = [
...(authProviderCheck(AuthProvider.GOOGLE, allowedAuthProviders)
? [GoogleStrategy]
: []),
...(authProviderCheck(AuthProvider.GITHUB, allowedAuthProviders)
? [GithubStrategy]
: []),
...(authProviderCheck(AuthProvider.MICROSOFT, allowedAuthProviders)
? [MicrosoftStrategy]
: []),
];
return {
module: AuthModule,
providers,
};
}
}

View File

@@ -21,15 +21,26 @@ import { VerifyMagicDto } from './dto/verify-magic.dto';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import * as argon2 from 'argon2'; import * as argon2 from 'argon2';
import * as E from 'fp-ts/Either'; import * as E from 'fp-ts/Either';
import { ConfigService } from '@nestjs/config';
import { InfraConfigService } from 'src/infra-config/infra-config.service';
const mockPrisma = mockDeep<PrismaService>(); const mockPrisma = mockDeep<PrismaService>();
const mockUser = mockDeep<UserService>(); const mockUser = mockDeep<UserService>();
const mockJWT = mockDeep<JwtService>(); const mockJWT = mockDeep<JwtService>();
const mockMailer = mockDeep<MailerService>(); const mockMailer = mockDeep<MailerService>();
const mockConfigService = mockDeep<ConfigService>();
const mockInfraConfigService = mockDeep<InfraConfigService>();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
const authService = new AuthService(mockUser, mockPrisma, mockJWT, mockMailer); const authService = new AuthService(
mockUser,
mockPrisma,
mockJWT,
mockMailer,
mockConfigService,
mockInfraConfigService,
);
const currentTime = new Date(); const currentTime = new Date();
@@ -91,6 +102,8 @@ describe('signInMagicLink', () => {
mockUser.createUserViaMagicLink.mockResolvedValue(user); mockUser.createUserViaMagicLink.mockResolvedValue(user);
// create new entry in VerificationToken table // create new entry in VerificationToken table
mockPrisma.verificationToken.create.mockResolvedValueOnce(passwordlessData); mockPrisma.verificationToken.create.mockResolvedValueOnce(passwordlessData);
// Read env variable 'MAGIC_LINK_TOKEN_VALIDITY' from config service
mockConfigService.get.mockReturnValue('3');
const result = await authService.signInMagicLink( const result = await authService.signInMagicLink(
'dwight@dundermifflin.com', 'dwight@dundermifflin.com',

View File

@@ -28,6 +28,8 @@ import { AuthError } from 'src/types/AuthError';
import { AuthUser, IsAdmin } from 'src/types/AuthUser'; import { AuthUser, IsAdmin } from 'src/types/AuthUser';
import { VerificationToken } from '@prisma/client'; import { VerificationToken } from '@prisma/client';
import { Origin } from './helper'; import { Origin } from './helper';
import { ConfigService } from '@nestjs/config';
import { InfraConfigService } from 'src/infra-config/infra-config.service';
@Injectable() @Injectable()
export class AuthService { export class AuthService {
@@ -36,6 +38,8 @@ export class AuthService {
private prismaService: PrismaService, private prismaService: PrismaService,
private jwtService: JwtService, private jwtService: JwtService,
private readonly mailerService: MailerService, private readonly mailerService: MailerService,
private readonly configService: ConfigService,
private infraConfigService: InfraConfigService,
) {} ) {}
/** /**
@@ -46,10 +50,12 @@ export class AuthService {
*/ */
private async generateMagicLinkTokens(user: AuthUser) { private async generateMagicLinkTokens(user: AuthUser) {
const salt = await bcrypt.genSalt( const salt = await bcrypt.genSalt(
parseInt(process.env.TOKEN_SALT_COMPLEXITY), parseInt(this.configService.get('TOKEN_SALT_COMPLEXITY')),
); );
const expiresOn = DateTime.now() const expiresOn = DateTime.now()
.plus({ hours: parseInt(process.env.MAGIC_LINK_TOKEN_VALIDITY) }) .plus({
hours: parseInt(this.configService.get('MAGIC_LINK_TOKEN_VALIDITY')),
})
.toISO() .toISO()
.toString(); .toString();
@@ -95,13 +101,13 @@ export class AuthService {
*/ */
private async generateRefreshToken(userUid: string) { private async generateRefreshToken(userUid: string) {
const refreshTokenPayload: RefreshTokenPayload = { const refreshTokenPayload: RefreshTokenPayload = {
iss: process.env.VITE_BASE_URL, iss: this.configService.get('VITE_BASE_URL'),
sub: userUid, sub: userUid,
aud: [process.env.VITE_BASE_URL], aud: [this.configService.get('VITE_BASE_URL')],
}; };
const refreshToken = await this.jwtService.sign(refreshTokenPayload, { const refreshToken = await this.jwtService.sign(refreshTokenPayload, {
expiresIn: process.env.REFRESH_TOKEN_VALIDITY, //7 Days expiresIn: this.configService.get('REFRESH_TOKEN_VALIDITY'), //7 Days
}); });
const refreshTokenHash = await argon2.hash(refreshToken); const refreshTokenHash = await argon2.hash(refreshToken);
@@ -127,9 +133,9 @@ export class AuthService {
*/ */
async generateAuthTokens(userUid: string) { async generateAuthTokens(userUid: string) {
const accessTokenPayload: AccessTokenPayload = { const accessTokenPayload: AccessTokenPayload = {
iss: process.env.VITE_BASE_URL, iss: this.configService.get('VITE_BASE_URL'),
sub: userUid, sub: userUid,
aud: [process.env.VITE_BASE_URL], aud: [this.configService.get('VITE_BASE_URL')],
}; };
const refreshToken = await this.generateRefreshToken(userUid); const refreshToken = await this.generateRefreshToken(userUid);
@@ -137,7 +143,7 @@ export class AuthService {
return E.right(<AuthTokens>{ return E.right(<AuthTokens>{
access_token: await this.jwtService.sign(accessTokenPayload, { access_token: await this.jwtService.sign(accessTokenPayload, {
expiresIn: process.env.ACCESS_TOKEN_VALIDITY, //1 Day expiresIn: this.configService.get('ACCESS_TOKEN_VALIDITY'), //1 Day
}), }),
refresh_token: refreshToken.right, refresh_token: refreshToken.right,
}); });
@@ -218,14 +224,14 @@ export class AuthService {
let url: string; let url: string;
switch (origin) { switch (origin) {
case Origin.ADMIN: case Origin.ADMIN:
url = process.env.VITE_ADMIN_URL; url = this.configService.get('VITE_ADMIN_URL');
break; break;
case Origin.APP: case Origin.APP:
url = process.env.VITE_BASE_URL; url = this.configService.get('VITE_BASE_URL');
break; break;
default: default:
// if origin is invalid by default set URL to Hoppscotch-App // if origin is invalid by default set URL to Hoppscotch-App
url = process.env.VITE_BASE_URL; url = this.configService.get('VITE_BASE_URL');
} }
await this.mailerService.sendEmail(email, { await this.mailerService.sendEmail(email, {
@@ -377,4 +383,8 @@ export class AuthService {
return E.right(<IsAdmin>{ isAdmin: false }); return E.right(<IsAdmin>{ isAdmin: false });
} }
getAuthProviders() {
return this.infraConfigService.getAllowedAuthProviders();
}
} }

View File

@@ -3,14 +3,25 @@ import { AuthGuard } from '@nestjs/passport';
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper'; import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors'; import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class GithubSSOGuard extends AuthGuard('github') implements CanActivate { export class GithubSSOGuard extends AuthGuard('github') implements CanActivate {
constructor(private readonly configService: ConfigService) {
super();
}
canActivate( canActivate(
context: ExecutionContext, context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> { ): boolean | Promise<boolean> | Observable<boolean> {
if (!authProviderCheck(AuthProvider.GITHUB)) if (
!authProviderCheck(
AuthProvider.GITHUB,
this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'),
)
) {
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 }); throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
}
return super.canActivate(context); return super.canActivate(context);
} }

View File

@@ -3,14 +3,25 @@ import { AuthGuard } from '@nestjs/passport';
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper'; import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors'; import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class GoogleSSOGuard extends AuthGuard('google') implements CanActivate { export class GoogleSSOGuard extends AuthGuard('google') implements CanActivate {
constructor(private readonly configService: ConfigService) {
super();
}
canActivate( canActivate(
context: ExecutionContext, context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> { ): boolean | Promise<boolean> | Observable<boolean> {
if (!authProviderCheck(AuthProvider.GOOGLE)) if (
!authProviderCheck(
AuthProvider.GOOGLE,
this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'),
)
) {
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 }); throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
}
return super.canActivate(context); return super.canActivate(context);
} }

View File

@@ -3,20 +3,31 @@ import { AuthGuard } from '@nestjs/passport';
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper'; import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors'; import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class MicrosoftSSOGuard export class MicrosoftSSOGuard
extends AuthGuard('microsoft') extends AuthGuard('microsoft')
implements CanActivate implements CanActivate
{ {
constructor(private readonly configService: ConfigService) {
super();
}
canActivate( canActivate(
context: ExecutionContext, context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> { ): boolean | Promise<boolean> | Observable<boolean> {
if (!authProviderCheck(AuthProvider.MICROSOFT)) if (
!authProviderCheck(
AuthProvider.MICROSOFT,
this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'),
)
) {
throwHTTPErr({ throwHTTPErr({
message: AUTH_PROVIDER_NOT_SPECIFIED, message: AUTH_PROVIDER_NOT_SPECIFIED,
statusCode: 404, statusCode: 404,
}); });
}
return super.canActivate(context); return super.canActivate(context);
} }

View File

@@ -6,6 +6,7 @@ import { Response } from 'express';
import * as cookie from 'cookie'; import * as cookie from 'cookie';
import { AUTH_PROVIDER_NOT_SPECIFIED, COOKIES_NOT_FOUND } from 'src/errors'; import { AUTH_PROVIDER_NOT_SPECIFIED, COOKIES_NOT_FOUND } from 'src/errors';
import { throwErr } from 'src/utils'; import { throwErr } from 'src/utils';
import { ConfigService } from '@nestjs/config';
enum AuthTokenType { enum AuthTokenType {
ACCESS_TOKEN = 'access_token', ACCESS_TOKEN = 'access_token',
@@ -45,15 +46,17 @@ export const authCookieHandler = (
redirect: boolean, redirect: boolean,
redirectUrl: string | null, redirectUrl: string | null,
) => { ) => {
const configService = new ConfigService();
const currentTime = DateTime.now(); const currentTime = DateTime.now();
const accessTokenValidity = currentTime const accessTokenValidity = currentTime
.plus({ .plus({
milliseconds: parseInt(process.env.ACCESS_TOKEN_VALIDITY), milliseconds: parseInt(configService.get('ACCESS_TOKEN_VALIDITY')),
}) })
.toMillis(); .toMillis();
const refreshTokenValidity = currentTime const refreshTokenValidity = currentTime
.plus({ .plus({
milliseconds: parseInt(process.env.REFRESH_TOKEN_VALIDITY), milliseconds: parseInt(configService.get('REFRESH_TOKEN_VALIDITY')),
}) })
.toMillis(); .toMillis();
@@ -75,10 +78,12 @@ export const authCookieHandler = (
} }
// check to see if redirectUrl is a whitelisted url // check to see if redirectUrl is a whitelisted url
const whitelistedOrigins = process.env.WHITELISTED_ORIGINS.split(','); const whitelistedOrigins = configService
.get('WHITELISTED_ORIGINS')
.split(',');
if (!whitelistedOrigins.includes(redirectUrl)) if (!whitelistedOrigins.includes(redirectUrl))
// if it is not redirect by default to REDIRECT_URL // if it is not redirect by default to REDIRECT_URL
redirectUrl = process.env.REDIRECT_URL; redirectUrl = configService.get('REDIRECT_URL');
return res.status(HttpStatus.OK).redirect(redirectUrl); return res.status(HttpStatus.OK).redirect(redirectUrl);
}; };
@@ -112,13 +117,16 @@ export const subscriptionContextCookieParser = (rawCookies: string) => {
* @param provider Provider we want to check the presence of * @param provider Provider we want to check the presence of
* @returns Boolean if provider specified is present or not * @returns Boolean if provider specified is present or not
*/ */
export function authProviderCheck(provider: string) { export function authProviderCheck(
provider: string,
VITE_ALLOWED_AUTH_PROVIDERS: string,
) {
if (!provider) { if (!provider) {
throwErr(AUTH_PROVIDER_NOT_SPECIFIED); throwErr(AUTH_PROVIDER_NOT_SPECIFIED);
} }
const envVariables = process.env.VITE_ALLOWED_AUTH_PROVIDERS const envVariables = VITE_ALLOWED_AUTH_PROVIDERS
? process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(',').map((provider) => ? VITE_ALLOWED_AUTH_PROVIDERS.split(',').map((provider) =>
provider.trim().toUpperCase(), provider.trim().toUpperCase(),
) )
: []; : [];

View File

@@ -5,18 +5,20 @@ import { AuthService } from '../auth.service';
import { UserService } from 'src/user/user.service'; import { UserService } from 'src/user/user.service';
import * as O from 'fp-ts/Option'; import * as O from 'fp-ts/Option';
import * as E from 'fp-ts/Either'; import * as E from 'fp-ts/Either';
import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class GithubStrategy extends PassportStrategy(Strategy) { export class GithubStrategy extends PassportStrategy(Strategy) {
constructor( constructor(
private authService: AuthService, private authService: AuthService,
private usersService: UserService, private usersService: UserService,
private configService: ConfigService,
) { ) {
super({ super({
clientID: process.env.GITHUB_CLIENT_ID, clientID: configService.get('INFRA.GITHUB_CLIENT_ID'),
clientSecret: process.env.GITHUB_CLIENT_SECRET, clientSecret: configService.get('INFRA.GITHUB_CLIENT_SECRET'),
callbackURL: process.env.GITHUB_CALLBACK_URL, callbackURL: configService.get('GITHUB_CALLBACK_URL'),
scope: [process.env.GITHUB_SCOPE], scope: [configService.get('GITHUB_SCOPE')],
store: true, store: true,
}); });
} }

View File

@@ -5,18 +5,20 @@ import { UserService } from 'src/user/user.service';
import * as O from 'fp-ts/Option'; import * as O from 'fp-ts/Option';
import { AuthService } from '../auth.service'; import { AuthService } from '../auth.service';
import * as E from 'fp-ts/Either'; import * as E from 'fp-ts/Either';
import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy) { export class GoogleStrategy extends PassportStrategy(Strategy) {
constructor( constructor(
private usersService: UserService, private usersService: UserService,
private authService: AuthService, private authService: AuthService,
private configService: ConfigService,
) { ) {
super({ super({
clientID: process.env.GOOGLE_CLIENT_ID, clientID: configService.get('INFRA.GOOGLE_CLIENT_ID'),
clientSecret: process.env.GOOGLE_CLIENT_SECRET, clientSecret: configService.get('INFRA.GOOGLE_CLIENT_SECRET'),
callbackURL: process.env.GOOGLE_CALLBACK_URL, callbackURL: configService.get('GOOGLE_CALLBACK_URL'),
scope: process.env.GOOGLE_SCOPE.split(','), scope: configService.get('GOOGLE_SCOPE').split(','),
passReqToCallback: true, passReqToCallback: true,
store: true, store: true,
}); });

View File

@@ -15,10 +15,14 @@ import {
INVALID_ACCESS_TOKEN, INVALID_ACCESS_TOKEN,
USER_NOT_FOUND, USER_NOT_FOUND,
} from 'src/errors'; } from 'src/errors';
import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(private usersService: UserService) { constructor(
private usersService: UserService,
private configService: ConfigService,
) {
super({ super({
jwtFromRequest: ExtractJwt.fromExtractors([ jwtFromRequest: ExtractJwt.fromExtractors([
(request: Request) => { (request: Request) => {
@@ -29,7 +33,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
return ATCookie; return ATCookie;
}, },
]), ]),
secretOrKey: process.env.JWT_SECRET, secretOrKey: configService.get('JWT_SECRET'),
}); });
} }

View File

@@ -5,19 +5,21 @@ import { AuthService } from '../auth.service';
import { UserService } from 'src/user/user.service'; import { UserService } from 'src/user/user.service';
import * as O from 'fp-ts/Option'; import * as O from 'fp-ts/Option';
import * as E from 'fp-ts/Either'; import * as E from 'fp-ts/Either';
import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class MicrosoftStrategy extends PassportStrategy(Strategy) { export class MicrosoftStrategy extends PassportStrategy(Strategy) {
constructor( constructor(
private authService: AuthService, private authService: AuthService,
private usersService: UserService, private usersService: UserService,
private configService: ConfigService,
) { ) {
super({ super({
clientID: process.env.MICROSOFT_CLIENT_ID, clientID: configService.get('INFRA.MICROSOFT_CLIENT_ID'),
clientSecret: process.env.MICROSOFT_CLIENT_SECRET, clientSecret: configService.get('INFRA.MICROSOFT_CLIENT_SECRET'),
callbackURL: process.env.MICROSOFT_CALLBACK_URL, callbackURL: configService.get('MICROSOFT_CALLBACK_URL'),
scope: [process.env.MICROSOFT_SCOPE], scope: [configService.get('MICROSOFT_SCOPE')],
tenant: process.env.MICROSOFT_TENANT, tenant: configService.get('MICROSOFT_TENANT'),
store: true, store: true,
}); });
} }

View File

@@ -14,10 +14,14 @@ import {
USER_NOT_FOUND, USER_NOT_FOUND,
} from 'src/errors'; } from 'src/errors';
import * as O from 'fp-ts/Option'; import * as O from 'fp-ts/Option';
import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class RTJwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') { export class RTJwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
constructor(private usersService: UserService) { constructor(
private usersService: UserService,
private configService: ConfigService,
) {
super({ super({
jwtFromRequest: ExtractJwt.fromExtractors([ jwtFromRequest: ExtractJwt.fromExtractors([
(request: Request) => { (request: Request) => {
@@ -28,7 +32,7 @@ export class RTJwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
return RTCookie; return RTCookie;
}, },
]), ]),
secretOrKey: process.env.JWT_SECRET, secretOrKey: configService.get('JWT_SECRET'),
}); });
} }

View File

@@ -644,3 +644,41 @@ export const SHORTCODE_INVALID_PROPERTIES_JSON =
*/ */
export const SHORTCODE_PROPERTIES_NOT_FOUND = export const SHORTCODE_PROPERTIES_NOT_FOUND =
'shortcode/properties_not_found' as const; 'shortcode/properties_not_found' as const;
/**
* Infra Config not found
* (InfraConfigService)
*/
export const INFRA_CONFIG_NOT_FOUND = 'infra_config/not_found' as const;
/**
* Infra Config update failed
* (InfraConfigService)
*/
export const INFRA_CONFIG_UPDATE_FAILED = 'infra_config/update_failed' as const;
/**
* Infra Config not listed for onModuleInit creation
* (InfraConfigService)
*/
export const INFRA_CONFIG_NOT_LISTED =
'infra_config/properly_not_listed' as const;
/**
* Infra Config reset failed
* (InfraConfigService)
*/
export const INFRA_CONFIG_RESET_FAILED = 'infra_config/reset_failed' as const;
/**
* Infra Config invalid input for Config variable
* (InfraConfigService)
*/
export const INFRA_CONFIG_INVALID_INPUT = 'infra_config/invalid_input' as const;
/**
* Error message for when the database table does not exist
* (InfraConfigService)
*/
export const DATABASE_TABLE_NOT_EXIST =
'Database migration not performed. Please check the FAQ for assistance: https://docs.hoppscotch.io/support/faq';

View File

@@ -0,0 +1,44 @@
import { PrismaService } from 'src/prisma/prisma.service';
export enum ServiceStatus {
ENABLE = 'ENABLE',
DISABLE = 'DISABLE',
}
/**
* Load environment variables from the database and set them in the process
*
* @Description Fetch the 'infra_config' table from the database and return it as an object
* (ConfigModule will set the environment variables in the process)
*/
export async function loadInfraConfiguration() {
try {
const prisma = new PrismaService();
const infraConfigs = await prisma.infraConfig.findMany();
let environmentObject: Record<string, any> = {};
infraConfigs.forEach((infraConfig) => {
environmentObject[infraConfig.name] = infraConfig.value;
});
return { INFRA: environmentObject };
} catch (error) {
// Prisma throw error if 'Can't reach at database server' OR 'Table does not exist'
// Reason for not throwing error is, we want successful build during 'postinstall' and generate dist files
return { INFRA: {} };
}
}
/**
* Stop the app after 5 seconds
* (Docker will re-start the app)
*/
export function stopApp() {
console.log('Stopping app in 5 seconds...');
setTimeout(() => {
console.log('Stopping app now...');
process.kill(process.pid, 'SIGTERM');
}, 5000);
}

View File

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

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { InfraConfigService } from './infra-config.service';
import { PrismaModule } from 'src/prisma/prisma.module';
@Module({
imports: [PrismaModule],
providers: [InfraConfigService],
exports: [InfraConfigService],
})
export class InfraConfigModule {}

View File

@@ -0,0 +1,109 @@
import { mockDeep, mockReset } from 'jest-mock-extended';
import { PrismaService } from 'src/prisma/prisma.service';
import { InfraConfigService } from './infra-config.service';
import {
InfraConfigEnum,
InfraConfigEnumForClient,
} from 'src/types/InfraConfig';
import { INFRA_CONFIG_NOT_FOUND, INFRA_CONFIG_UPDATE_FAILED } from 'src/errors';
import { ConfigService } from '@nestjs/config';
import * as helper from './helper';
const mockPrisma = mockDeep<PrismaService>();
const mockConfigService = mockDeep<ConfigService>();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const infraConfigService = new InfraConfigService(
mockPrisma,
mockConfigService,
);
beforeEach(() => {
mockReset(mockPrisma);
});
describe('InfraConfigService', () => {
describe('update', () => {
it('should update the infra config', 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(result).toEqualRight({ name, value });
});
it('should pass correct params to prisma update', async () => {
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
const value = 'true';
jest.spyOn(helper, 'stopApp').mockReturnValueOnce();
await infraConfigService.update(name, value);
expect(mockPrisma.infraConfig.update).toHaveBeenCalledWith({
where: { name },
data: { value },
});
expect(mockPrisma.infraConfig.update).toHaveBeenCalledTimes(1);
});
it('should throw an error if the infra config update failed', async () => {
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
const value = 'true';
mockPrisma.infraConfig.update.mockRejectedValueOnce('null');
const result = await infraConfigService.update(name, value);
expect(result).toEqualLeft(INFRA_CONFIG_UPDATE_FAILED);
});
});
describe('get', () => {
it('should get the infra config', async () => {
const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID;
const value = 'true';
mockPrisma.infraConfig.findUniqueOrThrow.mockResolvedValueOnce({
id: '',
name,
value,
active: true,
createdOn: new Date(),
updatedOn: new Date(),
});
const result = await infraConfigService.get(name);
expect(result).toEqualRight({ name, value });
});
it('should pass correct params to prisma findUnique', async () => {
const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID;
await infraConfigService.get(name);
expect(mockPrisma.infraConfig.findUniqueOrThrow).toHaveBeenCalledWith({
where: { name },
});
expect(mockPrisma.infraConfig.findUniqueOrThrow).toHaveBeenCalledTimes(1);
});
it('should throw an error if the infra config does not exist', async () => {
const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID;
mockPrisma.infraConfig.findUniqueOrThrow.mockRejectedValueOnce('null');
const result = await infraConfigService.get(name);
expect(result).toEqualLeft(INFRA_CONFIG_NOT_FOUND);
});
});
});

View File

@@ -0,0 +1,312 @@
import { Injectable, OnModuleInit } from '@nestjs/common';
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 {
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,
} from 'src/errors';
import { throwErr, validateEmail, validateSMTPUrl } from 'src/utils';
import { ConfigService } from '@nestjs/config';
import { ServiceStatus, stopApp } from './helper';
import { EnableAndDisableSSOArgs, InfraConfigArgs } from './input-args';
@Injectable()
export class InfraConfigService implements OnModuleInit {
constructor(
private readonly prisma: PrismaService,
private readonly configService: ConfigService,
) {}
async onModuleInit() {
await this.initializeInfraConfigTable();
}
getDefaultInfraConfigs(): { 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: process.env.VITE_ALLOWED_AUTH_PROVIDERS.toLocaleUpperCase(),
},
];
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 = 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);
}
// Eliminate the rows (from 'infraConfigDefaultObjs') that are already present in the database table
const dbInfraConfigs = await this.prisma.infraConfig.findMany();
const propsToInsert = infraConfigDefaultObjs.filter(
(p) => !dbInfraConfigs.find((e) => e.name === p.name),
);
if (propsToInsert.length > 0) {
await this.prisma.infraConfig.createMany({ data: propsToInsert });
stopApp();
}
} catch (error) {
if (error.code === 'P1001') {
// Prisma error code for 'Can't reach at database server'
// We're not throwing error here because we want to allow the app to run 'pnpm install'
} else if (error.code === 'P2021') {
// Prisma error code for 'Table does not exist'
throwErr(DATABASE_TABLE_NOT_EXIST);
} else {
throwErr(error);
}
}
}
/**
* Typecast a database InfraConfig to a InfraConfig model
* @param dbInfraConfig database InfraConfig
* @returns InfraConfig model
*/
cast(dbInfraConfig: DBInfraConfig) {
return <InfraConfig>{
name: dbInfraConfig.name,
value: dbInfraConfig.value,
};
}
/**
* Update InfraConfig by name
* @param name Name of the InfraConfig
* @param value Value of the InfraConfig
* @returns InfraConfig model
*/
async update(
name: InfraConfigEnumForClient | InfraConfigEnum,
value: string,
) {
const isValidate = this.validateEnvValues([{ name, value }]);
if (E.isLeft(isValidate)) return E.left(isValidate.left);
try {
const infraConfig = await this.prisma.infraConfig.update({
where: { name },
data: { value },
});
stopApp();
return E.right(this.cast(infraConfig));
} catch (e) {
return E.left(INFRA_CONFIG_UPDATE_FAILED);
}
}
/**
* Update InfraConfigs by name
* @param infraConfigs InfraConfigs to update
* @returns InfraConfig model
*/
async updateMany(infraConfigs: InfraConfigArgs[]) {
const isValidate = this.validateEnvValues(infraConfigs);
if (E.isLeft(isValidate)) return E.left(isValidate.left);
try {
await this.prisma.$transaction(async (tx) => {
for (let i = 0; i < infraConfigs.length; i++) {
await tx.infraConfig.update({
where: { name: infraConfigs[i].name },
data: { value: infraConfigs[i].value },
});
}
});
stopApp();
return E.right(infraConfigs);
} catch (e) {
return E.left(INFRA_CONFIG_UPDATE_FAILED);
}
}
/**
* Enable or Disable SSO for login/signup
* @param provider Auth Provider to enable or disable
* @param status Status to enable or disable
* @returns Either true or an error
*/
async enableAndDisableSSO(providerInfo: EnableAndDisableSSOArgs[]) {
const allowedAuthProviders = this.configService
.get<string>('INFRA.VITE_ALLOWED_AUTH_PROVIDERS')
.split(',');
let updatedAuthProviders = allowedAuthProviders;
providerInfo.forEach(({ provider, status }) => {
if (status === ServiceStatus.ENABLE) {
updatedAuthProviders.push(provider);
} else if (status === ServiceStatus.DISABLE) {
updatedAuthProviders = updatedAuthProviders.filter(
(p) => p !== provider,
);
}
});
updatedAuthProviders = [...new Set(updatedAuthProviders)];
if (updatedAuthProviders.length === 0) {
return E.left(AUTH_PROVIDER_NOT_SPECIFIED);
}
const isUpdated = await this.update(
InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
updatedAuthProviders.join(','),
);
if (E.isLeft(isUpdated)) return E.left(isUpdated.left);
return E.right(true);
}
/**
* Get InfraConfig by name
* @param name Name of the InfraConfig
* @returns InfraConfig model
*/
async get(name: InfraConfigEnumForClient) {
try {
const infraConfig = await this.prisma.infraConfig.findUniqueOrThrow({
where: { name },
});
return E.right(this.cast(infraConfig));
} catch (e) {
return E.left(INFRA_CONFIG_NOT_FOUND);
}
}
/**
* Get InfraConfigs by names
* @param names Names of the InfraConfigs
* @returns InfraConfig model
*/
async getMany(names: InfraConfigEnumForClient[]) {
try {
const infraConfigs = await this.prisma.infraConfig.findMany({
where: { name: { in: names } },
});
return E.right(infraConfigs.map((p) => this.cast(p)));
} catch (e) {
return E.left(INFRA_CONFIG_NOT_FOUND);
}
}
/**
* Get allowed auth providers for login/signup
* @returns string[]
*/
getAllowedAuthProviders() {
return this.configService
.get<string>('INFRA.VITE_ALLOWED_AUTH_PROVIDERS')
.split(',');
}
/**
* Reset all the InfraConfigs to their default values (from .env)
*/
async reset() {
try {
const infraConfigDefaultObjs = this.getDefaultInfraConfigs();
await this.prisma.infraConfig.deleteMany({
where: { name: { in: infraConfigDefaultObjs.map((p) => p.name) } },
});
await this.prisma.infraConfig.createMany({
data: infraConfigDefaultObjs,
});
stopApp();
return E.right(true);
} catch (e) {
return E.left(INFRA_CONFIG_RESET_FAILED);
}
}
validateEnvValues(
infraConfigs: {
name: InfraConfigEnumForClient | InfraConfigEnum;
value: string;
}[],
) {
for (let i = 0; i < infraConfigs.length; i++) {
switch (infraConfigs[i].name) {
case InfraConfigEnumForClient.MAILER_SMTP_URL:
const isValidUrl = validateSMTPUrl(infraConfigs[i].value);
if (!isValidUrl) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnumForClient.MAILER_ADDRESS_FROM:
const isValidEmail = validateEmail(infraConfigs[i].value);
if (!isValidEmail) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
default:
break;
}
}
return E.right(true);
}
}

View File

@@ -0,0 +1,30 @@
import { Field, InputType } from '@nestjs/graphql';
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
import { ServiceStatus } from './helper';
import { AuthProvider } from 'src/auth/helper';
@InputType()
export class InfraConfigArgs {
@Field(() => InfraConfigEnumForClient, {
description: 'Infra Config Name',
})
name: InfraConfigEnumForClient;
@Field({
description: 'Infra Config Value',
})
value: string;
}
@InputType()
export class EnableAndDisableSSOArgs {
@Field(() => AuthProvider, {
description: 'Auth Provider',
})
provider: AuthProvider;
@Field(() => ServiceStatus, {
description: 'Auth Provider Status',
})
status: ServiceStatus;
}

View File

@@ -1,4 +1,4 @@
import { Module } from '@nestjs/common'; import { Global, Module } from '@nestjs/common';
import { MailerModule as NestMailerModule } from '@nestjs-modules/mailer'; import { MailerModule as NestMailerModule } from '@nestjs-modules/mailer';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter'; import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
import { MailerService } from './mailer.service'; import { MailerService } from './mailer.service';
@@ -7,24 +7,42 @@ import {
MAILER_FROM_ADDRESS_UNDEFINED, MAILER_FROM_ADDRESS_UNDEFINED,
MAILER_SMTP_URL_UNDEFINED, MAILER_SMTP_URL_UNDEFINED,
} from 'src/errors'; } from 'src/errors';
import { ConfigService } from '@nestjs/config';
import { loadInfraConfiguration } from 'src/infra-config/helper';
@Global()
@Module({ @Module({
imports: [ imports: [],
NestMailerModule.forRoot({
transport:
process.env.MAILER_SMTP_URL ?? throwErr(MAILER_SMTP_URL_UNDEFINED),
defaults: {
from:
process.env.MAILER_ADDRESS_FROM ??
throwErr(MAILER_FROM_ADDRESS_UNDEFINED),
},
template: {
dir: __dirname + '/templates',
adapter: new HandlebarsAdapter(),
},
}),
],
providers: [MailerService], providers: [MailerService],
exports: [MailerService], exports: [MailerService],
}) })
export class MailerModule {} export class MailerModule {
static async register() {
const env = await loadInfraConfiguration();
let mailerSmtpUrl = env.INFRA.MAILER_SMTP_URL;
let mailerAddressFrom = env.INFRA.MAILER_ADDRESS_FROM;
if (!env.INFRA.MAILER_SMTP_URL || !env.INFRA.MAILER_ADDRESS_FROM) {
const config = new ConfigService();
mailerSmtpUrl = config.get('MAILER_SMTP_URL');
mailerAddressFrom = config.get('MAILER_ADDRESS_FROM');
}
return {
module: MailerModule,
imports: [
NestMailerModule.forRoot({
transport: mailerSmtpUrl ?? throwErr(MAILER_SMTP_URL_UNDEFINED),
defaults: {
from: mailerAddressFrom ?? throwErr(MAILER_FROM_ADDRESS_UNDEFINED),
},
template: {
dir: __dirname + '/templates',
adapter: new HandlebarsAdapter(),
},
}),
],
};
}
}

View File

@@ -6,18 +6,23 @@ import { VersioningType } from '@nestjs/common';
import * as session from 'express-session'; import * as session from 'express-session';
import { emitGQLSchemaFile } from './gql-schema'; import { emitGQLSchemaFile } from './gql-schema';
import { checkEnvironmentAuthProvider } from './utils'; import { checkEnvironmentAuthProvider } from './utils';
import { ConfigService } from '@nestjs/config';
async function bootstrap() { async function bootstrap() {
console.log(`Running in production: ${process.env.PRODUCTION}`);
console.log(`Port: ${process.env.PORT}`);
checkEnvironmentAuthProvider();
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService);
console.log(`Running in production: ${configService.get('PRODUCTION')}`);
console.log(`Port: ${configService.get('PORT')}`);
checkEnvironmentAuthProvider(
configService.get('VITE_ALLOWED_AUTH_PROVIDERS'),
);
app.use( app.use(
session({ session({
secret: process.env.SESSION_SECRET, secret: configService.get('SESSION_SECRET'),
}), }),
); );
@@ -28,18 +33,18 @@ async function bootstrap() {
}), }),
); );
if (process.env.PRODUCTION === 'false') { if (configService.get('PRODUCTION') === 'false') {
console.log('Enabling CORS with development settings'); console.log('Enabling CORS with development settings');
app.enableCors({ app.enableCors({
origin: process.env.WHITELISTED_ORIGINS.split(','), origin: configService.get('WHITELISTED_ORIGINS').split(','),
credentials: true, credentials: true,
}); });
} else { } else {
console.log('Enabling CORS with production settings'); console.log('Enabling CORS with production settings');
app.enableCors({ app.enableCors({
origin: process.env.WHITELISTED_ORIGINS.split(','), origin: configService.get('WHITELISTED_ORIGINS').split(','),
credentials: true, credentials: true,
}); });
} }
@@ -47,7 +52,13 @@ async function bootstrap() {
type: VersioningType.URI, type: VersioningType.URI,
}); });
app.use(cookieParser()); app.use(cookieParser());
await app.listen(process.env.PORT || 3170); await app.listen(configService.get('PORT') || 3170);
// Graceful shutdown
process.on('SIGTERM', async () => {
console.info('SIGTERM signal received');
await app.close();
});
} }
if (!process.env.GENERATE_GQL_SCHEMA) { if (!process.env.GENERATE_GQL_SCHEMA) {

View File

@@ -504,20 +504,24 @@ describe('ShortcodeService', () => {
); );
expect(result).toEqual(<ShortcodeWithUserEmail[]>[ expect(result).toEqual(<ShortcodeWithUserEmail[]>[
{ {
id: shortcodes[0].id, id: shortcodesWithUserEmail[0].id,
request: JSON.stringify(shortcodes[0].request), request: JSON.stringify(shortcodesWithUserEmail[0].request),
properties: JSON.stringify(shortcodes[0].embedProperties), properties: JSON.stringify(
createdOn: shortcodes[0].createdOn, shortcodesWithUserEmail[0].embedProperties,
),
createdOn: shortcodesWithUserEmail[0].createdOn,
creator: { creator: {
uid: user.uid, uid: user.uid,
email: user.email, email: user.email,
}, },
}, },
{ {
id: shortcodes[1].id, id: shortcodesWithUserEmail[1].id,
request: JSON.stringify(shortcodes[1].request), request: JSON.stringify(shortcodesWithUserEmail[1].request),
properties: JSON.stringify(shortcodes[1].embedProperties), properties: JSON.stringify(
createdOn: shortcodes[1].createdOn, shortcodesWithUserEmail[1].embedProperties,
),
createdOn: shortcodesWithUserEmail[1].createdOn,
creator: { creator: {
uid: user.uid, uid: user.uid,
email: user.email, email: user.email,

View File

@@ -1,5 +1,4 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { MailerModule } from 'src/mailer/mailer.module';
import { PrismaModule } from 'src/prisma/prisma.module'; import { PrismaModule } from 'src/prisma/prisma.module';
import { PubSubModule } from 'src/pubsub/pubsub.module'; import { PubSubModule } from 'src/pubsub/pubsub.module';
import { TeamModule } from 'src/team/team.module'; import { TeamModule } from 'src/team/team.module';
@@ -12,7 +11,7 @@ import { TeamInviteeGuard } from './team-invitee.guard';
import { TeamTeamInviteExtResolver } from './team-teaminvite-ext.resolver'; import { TeamTeamInviteExtResolver } from './team-teaminvite-ext.resolver';
@Module({ @Module({
imports: [PrismaModule, TeamModule, PubSubModule, UserModule, MailerModule], imports: [PrismaModule, TeamModule, PubSubModule, UserModule],
providers: [ providers: [
TeamInvitationService, TeamInvitationService,
TeamInvitationResolver, TeamInvitationResolver,

View File

@@ -20,6 +20,7 @@ import { UserService } from 'src/user/user.service';
import { PubSubService } from 'src/pubsub/pubsub.service'; import { PubSubService } from 'src/pubsub/pubsub.service';
import { validateEmail } from '../utils'; import { validateEmail } from '../utils';
import { AuthUser } from 'src/types/AuthUser'; import { AuthUser } from 'src/types/AuthUser';
import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class TeamInvitationService { export class TeamInvitationService {
@@ -28,8 +29,8 @@ export class TeamInvitationService {
private readonly userService: UserService, private readonly userService: UserService,
private readonly teamService: TeamService, private readonly teamService: TeamService,
private readonly mailerService: MailerService, private readonly mailerService: MailerService,
private readonly pubsub: PubSubService, private readonly pubsub: PubSubService,
private readonly configService: ConfigService,
) {} ) {}
/** /**
@@ -150,7 +151,9 @@ export class TeamInvitationService {
template: 'team-invitation', template: 'team-invitation',
variables: { variables: {
invitee: creator.displayName ?? 'A Hoppscotch User', invitee: creator.displayName ?? 'A Hoppscotch User',
action_url: `${process.env.VITE_BASE_URL}/join-team?id=${dbInvitation.id}`, action_url: `${this.configService.get('VITE_BASE_URL')}/join-team?id=${
dbInvitation.id
}`,
invite_team_name: team.name, invite_team_name: team.name,
}, },
}); });

View File

@@ -0,0 +1,29 @@
export enum InfraConfigEnum {
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',
VITE_ALLOWED_AUTH_PROVIDERS = 'VITE_ALLOWED_AUTH_PROVIDERS',
}
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',
}

View File

@@ -131,6 +131,26 @@ export const validateEmail = (email: string) => {
).test(email); ).test(email);
}; };
/**
* Checks to see if the URL is valid or not
* @param url The URL to validate
* @returns boolean
*/
export const validateSMTPUrl = (url: string) => {
// Possible valid formats
// smtp(s)://mail.example.com
// smtp(s)://user:pass@mail.example.com
// smtp(s)://mail.example.com:587
// smtp(s)://user:pass@mail.example.com:587
if (!url || url.length === 0) return false;
const regex =
/^(smtp|smtps):\/\/(?:([^:]+):([^@]+)@)?((?!\.)[^:]+)(?::(\d+))?$/;
if (regex.test(url)) return true;
return false;
};
/** /**
* String to JSON parser * String to JSON parser
* @param {str} str The string to parse * @param {str} str The string to parse
@@ -161,21 +181,23 @@ export function isValidLength(title: string, length: number) {
/** /**
* This function is called by bootstrap() in main.ts * This function is called by bootstrap() in main.ts
* It checks if the "VITE_ALLOWED_AUTH_PROVIDERS" environment variable is properly set or not. * It checks if the "VITE_ALLOWED_AUTH_PROVIDERS" environment variable is properly set or not.
* If not, it throws an error. * If not, it throws an error.
*/ */
export function checkEnvironmentAuthProvider() { export function checkEnvironmentAuthProvider(
if (!process.env.hasOwnProperty('VITE_ALLOWED_AUTH_PROVIDERS')) { VITE_ALLOWED_AUTH_PROVIDERS: string,
) {
if (!VITE_ALLOWED_AUTH_PROVIDERS) {
throw new Error(ENV_NOT_FOUND_KEY_AUTH_PROVIDERS); throw new Error(ENV_NOT_FOUND_KEY_AUTH_PROVIDERS);
} }
if (process.env.VITE_ALLOWED_AUTH_PROVIDERS === '') { if (VITE_ALLOWED_AUTH_PROVIDERS === '') {
throw new Error(ENV_EMPTY_AUTH_PROVIDERS); throw new Error(ENV_EMPTY_AUTH_PROVIDERS);
} }
const givenAuthProviders = process.env.VITE_ALLOWED_AUTH_PROVIDERS.split( const givenAuthProviders = VITE_ALLOWED_AUTH_PROVIDERS.split(',').map(
',', (provider) => provider.toLocaleUpperCase(),
).map((provider) => provider.toLocaleUpperCase()); );
const supportedAuthProviders = Object.values(AuthProvider).map( const supportedAuthProviders = Object.values(AuthProvider).map(
(provider: string) => provider.toLocaleUpperCase(), (provider: string) => provider.toLocaleUpperCase(),
); );

1166
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff