Compare commits
29 Commits
refactor/w
...
feat/serve
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eec5b8f9c8 | ||
|
|
99d369fc79 | ||
|
|
7da5063a8c | ||
|
|
1957556b5a | ||
|
|
84fdcaf840 | ||
|
|
1f684d47e2 | ||
|
|
59dcd57d9c | ||
|
|
fc7817780d | ||
|
|
0f7aa5e84d | ||
|
|
85dea99452 | ||
|
|
94ca981144 | ||
|
|
8035d1d592 | ||
|
|
02dcc018fa | ||
|
|
11e2e18aa3 | ||
|
|
97d71e5032 | ||
|
|
180b9d776f | ||
|
|
c4a9038579 | ||
|
|
8df8b056d9 | ||
|
|
01ccb321f1 | ||
|
|
9f62ed6c90 | ||
|
|
ef8412544a | ||
|
|
1d05a76b59 | ||
|
|
6157848f56 | ||
|
|
0a10f7c654 | ||
|
|
4e188f3c11 | ||
|
|
c3522025c8 | ||
|
|
74cec0365b | ||
|
|
5bee3471c5 | ||
|
|
49c2ee0d38 |
@@ -66,6 +66,7 @@ services:
|
||||
# The service that spins up all 3 services at once in one container
|
||||
hoppscotch-aio:
|
||||
container_name: hoppscotch-aio
|
||||
restart: unless-stopped
|
||||
build:
|
||||
dockerfile: prod.Dockerfile
|
||||
context: .
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"@nestjs-modules/mailer": "^1.9.1",
|
||||
"@nestjs/apollo": "^12.0.9",
|
||||
"@nestjs/common": "^10.2.6",
|
||||
"@nestjs/config": "^3.1.1",
|
||||
"@nestjs/core": "^10.2.6",
|
||||
"@nestjs/graphql": "^12.0.9",
|
||||
"@nestjs/jwt": "^10.1.1",
|
||||
|
||||
@@ -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");
|
||||
@@ -209,3 +209,12 @@ enum TeamMemberRole {
|
||||
VIEWER
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { AdminService } from './admin.service';
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
import { PubSubModule } from '../pubsub/pubsub.module';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { MailerModule } from '../mailer/mailer.module';
|
||||
import { TeamModule } from '../team/team.module';
|
||||
import { TeamInvitationModule } from '../team-invitation/team-invitation.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 { InfraResolver } from './infra.resolver';
|
||||
import { ShortcodeModule } from 'src/shortcode/shortcode.module';
|
||||
import { InfraConfigModule } from 'src/infra-config/infra-config.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
PrismaModule,
|
||||
PubSubModule,
|
||||
UserModule,
|
||||
MailerModule,
|
||||
TeamModule,
|
||||
TeamInvitationModule,
|
||||
TeamEnvironmentsModule,
|
||||
TeamCollectionModule,
|
||||
TeamRequestModule,
|
||||
ShortcodeModule,
|
||||
InfraConfigModule,
|
||||
],
|
||||
providers: [InfraResolver, AdminResolver, AdminService],
|
||||
exports: [AdminService],
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
USER_ALREADY_INVITED,
|
||||
} from '../errors';
|
||||
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
const mockPrisma = mockDeep<PrismaService>();
|
||||
const mockPubSub = mockDeep<PubSubService>();
|
||||
@@ -27,6 +28,7 @@ const mockTeamInvitationService = mockDeep<TeamInvitationService>();
|
||||
const mockTeamCollectionService = mockDeep<TeamCollectionService>();
|
||||
const mockMailerService = mockDeep<MailerService>();
|
||||
const mockShortcodeService = mockDeep<ShortcodeService>();
|
||||
const mockConfigService = mockDeep<ConfigService>();
|
||||
|
||||
const adminService = new AdminService(
|
||||
mockUserService,
|
||||
@@ -39,6 +41,7 @@ const adminService = new AdminService(
|
||||
mockPrisma as any,
|
||||
mockMailerService,
|
||||
mockShortcodeService,
|
||||
mockConfigService,
|
||||
);
|
||||
|
||||
const invitedUsers: InvitedUsers[] = [
|
||||
|
||||
@@ -25,6 +25,7 @@ import { TeamEnvironmentsService } from '../team-environments/team-environments.
|
||||
import { TeamInvitationService } from '../team-invitation/team-invitation.service';
|
||||
import { TeamMemberRole } from '../team/team.model';
|
||||
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class AdminService {
|
||||
@@ -39,6 +40,7 @@ export class AdminService {
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly mailerService: MailerService,
|
||||
private readonly shortcodeService: ShortcodeService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -79,7 +81,7 @@ export class AdminService {
|
||||
template: 'user-invitation',
|
||||
variables: {
|
||||
inviteeEmail: inviteeEmail,
|
||||
magicLink: `${process.env.VITE_BASE_URL}`,
|
||||
magicLink: `${this.configService.get('VITE_BASE_URL')}`,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
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 { Infra } from './infra.model';
|
||||
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 { GqlAdmin } from './decorators/gql-admin.decorator';
|
||||
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)
|
||||
@Resolver(() => Infra)
|
||||
export class InfraResolver {
|
||||
constructor(private adminService: AdminService) {}
|
||||
constructor(
|
||||
private adminService: AdminService,
|
||||
private infraConfigService: InfraConfigService,
|
||||
) {}
|
||||
|
||||
@Query(() => Infra, {
|
||||
description: 'Fetch details of the Infrastructure',
|
||||
@@ -222,4 +239,76 @@ export class InfraResolver {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,51 +20,69 @@ import { ShortcodeModule } from './shortcode/shortcode.module';
|
||||
import { COOKIES_NOT_FOUND } from './errors';
|
||||
import { ThrottlerModule } from '@nestjs/throttler';
|
||||
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({
|
||||
imports: [
|
||||
GraphQLModule.forRoot<ApolloDriverConfig>({
|
||||
buildSchemaOptions: {
|
||||
numberScalarMode: 'integer',
|
||||
},
|
||||
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,
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
load: [async () => loadInfraConfiguration()],
|
||||
}),
|
||||
ThrottlerModule.forRoot([
|
||||
{
|
||||
ttl: +process.env.RATE_LIMIT_TTL,
|
||||
limit: +process.env.RATE_LIMIT_MAX,
|
||||
GraphQLModule.forRootAsync<ApolloDriverConfig>({
|
||||
driver: ApolloDriver,
|
||||
imports: [ConfigModule],
|
||||
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,
|
||||
AuthModule,
|
||||
AuthModule.register(),
|
||||
AdminModule,
|
||||
UserSettingsModule,
|
||||
UserEnvironmentsModule,
|
||||
@@ -77,6 +95,7 @@ import { AppController } from './app.controller';
|
||||
TeamInvitationModule,
|
||||
UserCollectionModule,
|
||||
ShortcodeModule,
|
||||
InfraConfigModule,
|
||||
],
|
||||
providers: [GQLComplexityPlugin],
|
||||
controllers: [AppController],
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
InternalServerErrorException,
|
||||
Post,
|
||||
Query,
|
||||
Request,
|
||||
@@ -31,11 +30,21 @@ import { MicrosoftSSOGuard } from './guards/microsoft-sso-.guard';
|
||||
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
|
||||
import { SkipThrottle } from '@nestjs/throttler';
|
||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@UseGuards(ThrottlerBehindProxyGuard)
|
||||
@Controller({ path: 'auth', version: '1' })
|
||||
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
|
||||
@@ -45,8 +54,14 @@ export class AuthController {
|
||||
@Body() authData: SignInMagicDto,
|
||||
@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 });
|
||||
}
|
||||
|
||||
const deviceIdToken = await this.authService.signInMagicLink(
|
||||
authData.email,
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Module } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { UserModule } from 'src/user/user.module';
|
||||
import { MailerModule } from 'src/mailer/mailer.module';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
@@ -12,25 +11,47 @@ import { GoogleStrategy } from './strategies/google.strategy';
|
||||
import { GithubStrategy } from './strategies/github.strategy';
|
||||
import { MicrosoftStrategy } from './strategies/microsoft.strategy';
|
||||
import { AuthProvider, authProviderCheck } from './helper';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { loadInfraConfiguration } from 'src/infra-config/helper';
|
||||
import { InfraConfigModule } from 'src/infra-config/infra-config.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
PrismaModule,
|
||||
UserModule,
|
||||
MailerModule,
|
||||
PassportModule,
|
||||
JwtModule.register({
|
||||
secret: process.env.JWT_SECRET,
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
secret: configService.get('JWT_SECRET'),
|
||||
}),
|
||||
}),
|
||||
InfraConfigModule,
|
||||
],
|
||||
providers: [
|
||||
AuthService,
|
||||
JwtStrategy,
|
||||
RTJwtStrategy,
|
||||
...(authProviderCheck(AuthProvider.GOOGLE) ? [GoogleStrategy] : []),
|
||||
...(authProviderCheck(AuthProvider.GITHUB) ? [GithubStrategy] : []),
|
||||
...(authProviderCheck(AuthProvider.MICROSOFT) ? [MicrosoftStrategy] : []),
|
||||
],
|
||||
providers: [AuthService, JwtStrategy, RTJwtStrategy],
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,15 +21,26 @@ import { VerifyMagicDto } from './dto/verify-magic.dto';
|
||||
import { DateTime } from 'luxon';
|
||||
import * as argon2 from 'argon2';
|
||||
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 mockUser = mockDeep<UserService>();
|
||||
const mockJWT = mockDeep<JwtService>();
|
||||
const mockMailer = mockDeep<MailerService>();
|
||||
const mockConfigService = mockDeep<ConfigService>();
|
||||
const mockInfraConfigService = mockDeep<InfraConfigService>();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const authService = new AuthService(mockUser, mockPrisma, mockJWT, mockMailer);
|
||||
const authService = new AuthService(
|
||||
mockUser,
|
||||
mockPrisma,
|
||||
mockJWT,
|
||||
mockMailer,
|
||||
mockConfigService,
|
||||
mockInfraConfigService,
|
||||
);
|
||||
|
||||
const currentTime = new Date();
|
||||
|
||||
@@ -91,6 +102,8 @@ describe('signInMagicLink', () => {
|
||||
mockUser.createUserViaMagicLink.mockResolvedValue(user);
|
||||
// create new entry in VerificationToken table
|
||||
mockPrisma.verificationToken.create.mockResolvedValueOnce(passwordlessData);
|
||||
// Read env variable 'MAGIC_LINK_TOKEN_VALIDITY' from config service
|
||||
mockConfigService.get.mockReturnValue('3');
|
||||
|
||||
const result = await authService.signInMagicLink(
|
||||
'dwight@dundermifflin.com',
|
||||
|
||||
@@ -28,6 +28,8 @@ import { AuthError } from 'src/types/AuthError';
|
||||
import { AuthUser, IsAdmin } from 'src/types/AuthUser';
|
||||
import { VerificationToken } from '@prisma/client';
|
||||
import { Origin } from './helper';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { InfraConfigService } from 'src/infra-config/infra-config.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@@ -36,6 +38,8 @@ export class AuthService {
|
||||
private prismaService: PrismaService,
|
||||
private jwtService: JwtService,
|
||||
private readonly mailerService: MailerService,
|
||||
private readonly configService: ConfigService,
|
||||
private infraConfigService: InfraConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -46,10 +50,12 @@ export class AuthService {
|
||||
*/
|
||||
private async generateMagicLinkTokens(user: AuthUser) {
|
||||
const salt = await bcrypt.genSalt(
|
||||
parseInt(process.env.TOKEN_SALT_COMPLEXITY),
|
||||
parseInt(this.configService.get('TOKEN_SALT_COMPLEXITY')),
|
||||
);
|
||||
const expiresOn = DateTime.now()
|
||||
.plus({ hours: parseInt(process.env.MAGIC_LINK_TOKEN_VALIDITY) })
|
||||
.plus({
|
||||
hours: parseInt(this.configService.get('MAGIC_LINK_TOKEN_VALIDITY')),
|
||||
})
|
||||
.toISO()
|
||||
.toString();
|
||||
|
||||
@@ -95,13 +101,13 @@ export class AuthService {
|
||||
*/
|
||||
private async generateRefreshToken(userUid: string) {
|
||||
const refreshTokenPayload: RefreshTokenPayload = {
|
||||
iss: process.env.VITE_BASE_URL,
|
||||
iss: this.configService.get('VITE_BASE_URL'),
|
||||
sub: userUid,
|
||||
aud: [process.env.VITE_BASE_URL],
|
||||
aud: [this.configService.get('VITE_BASE_URL')],
|
||||
};
|
||||
|
||||
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);
|
||||
@@ -127,9 +133,9 @@ export class AuthService {
|
||||
*/
|
||||
async generateAuthTokens(userUid: string) {
|
||||
const accessTokenPayload: AccessTokenPayload = {
|
||||
iss: process.env.VITE_BASE_URL,
|
||||
iss: this.configService.get('VITE_BASE_URL'),
|
||||
sub: userUid,
|
||||
aud: [process.env.VITE_BASE_URL],
|
||||
aud: [this.configService.get('VITE_BASE_URL')],
|
||||
};
|
||||
|
||||
const refreshToken = await this.generateRefreshToken(userUid);
|
||||
@@ -137,7 +143,7 @@ export class AuthService {
|
||||
|
||||
return E.right(<AuthTokens>{
|
||||
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,
|
||||
});
|
||||
@@ -218,14 +224,14 @@ export class AuthService {
|
||||
let url: string;
|
||||
switch (origin) {
|
||||
case Origin.ADMIN:
|
||||
url = process.env.VITE_ADMIN_URL;
|
||||
url = this.configService.get('VITE_ADMIN_URL');
|
||||
break;
|
||||
case Origin.APP:
|
||||
url = process.env.VITE_BASE_URL;
|
||||
url = this.configService.get('VITE_BASE_URL');
|
||||
break;
|
||||
default:
|
||||
// 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, {
|
||||
@@ -377,4 +383,8 @@ export class AuthService {
|
||||
|
||||
return E.right(<IsAdmin>{ isAdmin: false });
|
||||
}
|
||||
|
||||
getAuthProviders() {
|
||||
return this.infraConfigService.getAllowedAuthProviders();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,25 @@ import { AuthGuard } from '@nestjs/passport';
|
||||
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
||||
import { Observable } from 'rxjs';
|
||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class GithubSSOGuard extends AuthGuard('github') implements CanActivate {
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
super();
|
||||
}
|
||||
|
||||
canActivate(
|
||||
context: ExecutionContext,
|
||||
): 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 });
|
||||
}
|
||||
|
||||
return super.canActivate(context);
|
||||
}
|
||||
|
||||
@@ -3,14 +3,25 @@ import { AuthGuard } from '@nestjs/passport';
|
||||
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
||||
import { Observable } from 'rxjs';
|
||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class GoogleSSOGuard extends AuthGuard('google') implements CanActivate {
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
super();
|
||||
}
|
||||
|
||||
canActivate(
|
||||
context: ExecutionContext,
|
||||
): 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 });
|
||||
}
|
||||
|
||||
return super.canActivate(context);
|
||||
}
|
||||
|
||||
@@ -3,20 +3,31 @@ import { AuthGuard } from '@nestjs/passport';
|
||||
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
||||
import { Observable } from 'rxjs';
|
||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class MicrosoftSSOGuard
|
||||
extends AuthGuard('microsoft')
|
||||
implements CanActivate
|
||||
{
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
super();
|
||||
}
|
||||
|
||||
canActivate(
|
||||
context: ExecutionContext,
|
||||
): boolean | Promise<boolean> | Observable<boolean> {
|
||||
if (!authProviderCheck(AuthProvider.MICROSOFT))
|
||||
if (
|
||||
!authProviderCheck(
|
||||
AuthProvider.MICROSOFT,
|
||||
this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'),
|
||||
)
|
||||
) {
|
||||
throwHTTPErr({
|
||||
message: AUTH_PROVIDER_NOT_SPECIFIED,
|
||||
statusCode: 404,
|
||||
});
|
||||
}
|
||||
|
||||
return super.canActivate(context);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Response } from 'express';
|
||||
import * as cookie from 'cookie';
|
||||
import { AUTH_PROVIDER_NOT_SPECIFIED, COOKIES_NOT_FOUND } from 'src/errors';
|
||||
import { throwErr } from 'src/utils';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
enum AuthTokenType {
|
||||
ACCESS_TOKEN = 'access_token',
|
||||
@@ -45,15 +46,17 @@ export const authCookieHandler = (
|
||||
redirect: boolean,
|
||||
redirectUrl: string | null,
|
||||
) => {
|
||||
const configService = new ConfigService();
|
||||
|
||||
const currentTime = DateTime.now();
|
||||
const accessTokenValidity = currentTime
|
||||
.plus({
|
||||
milliseconds: parseInt(process.env.ACCESS_TOKEN_VALIDITY),
|
||||
milliseconds: parseInt(configService.get('ACCESS_TOKEN_VALIDITY')),
|
||||
})
|
||||
.toMillis();
|
||||
const refreshTokenValidity = currentTime
|
||||
.plus({
|
||||
milliseconds: parseInt(process.env.REFRESH_TOKEN_VALIDITY),
|
||||
milliseconds: parseInt(configService.get('REFRESH_TOKEN_VALIDITY')),
|
||||
})
|
||||
.toMillis();
|
||||
|
||||
@@ -75,10 +78,12 @@ export const authCookieHandler = (
|
||||
}
|
||||
|
||||
// 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 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);
|
||||
};
|
||||
@@ -112,13 +117,16 @@ export const subscriptionContextCookieParser = (rawCookies: string) => {
|
||||
* @param provider Provider we want to check the presence of
|
||||
* @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) {
|
||||
throwErr(AUTH_PROVIDER_NOT_SPECIFIED);
|
||||
}
|
||||
|
||||
const envVariables = process.env.VITE_ALLOWED_AUTH_PROVIDERS
|
||||
? process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(',').map((provider) =>
|
||||
const envVariables = VITE_ALLOWED_AUTH_PROVIDERS
|
||||
? VITE_ALLOWED_AUTH_PROVIDERS.split(',').map((provider) =>
|
||||
provider.trim().toUpperCase(),
|
||||
)
|
||||
: [];
|
||||
|
||||
@@ -5,18 +5,20 @@ import { AuthService } from '../auth.service';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
import * as O from 'fp-ts/Option';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class GithubStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private usersService: UserService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
super({
|
||||
clientID: process.env.GITHUB_CLIENT_ID,
|
||||
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
||||
callbackURL: process.env.GITHUB_CALLBACK_URL,
|
||||
scope: [process.env.GITHUB_SCOPE],
|
||||
clientID: configService.get('INFRA.GITHUB_CLIENT_ID'),
|
||||
clientSecret: configService.get('INFRA.GITHUB_CLIENT_SECRET'),
|
||||
callbackURL: configService.get('GITHUB_CALLBACK_URL'),
|
||||
scope: [configService.get('GITHUB_SCOPE')],
|
||||
store: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,18 +5,20 @@ import { UserService } from 'src/user/user.service';
|
||||
import * as O from 'fp-ts/Option';
|
||||
import { AuthService } from '../auth.service';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class GoogleStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(
|
||||
private usersService: UserService,
|
||||
private authService: AuthService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
super({
|
||||
clientID: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
callbackURL: process.env.GOOGLE_CALLBACK_URL,
|
||||
scope: process.env.GOOGLE_SCOPE.split(','),
|
||||
clientID: configService.get('INFRA.GOOGLE_CLIENT_ID'),
|
||||
clientSecret: configService.get('INFRA.GOOGLE_CLIENT_SECRET'),
|
||||
callbackURL: configService.get('GOOGLE_CALLBACK_URL'),
|
||||
scope: configService.get('GOOGLE_SCOPE').split(','),
|
||||
passReqToCallback: true,
|
||||
store: true,
|
||||
});
|
||||
|
||||
@@ -15,10 +15,14 @@ import {
|
||||
INVALID_ACCESS_TOKEN,
|
||||
USER_NOT_FOUND,
|
||||
} from 'src/errors';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
constructor(private usersService: UserService) {
|
||||
constructor(
|
||||
private usersService: UserService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromExtractors([
|
||||
(request: Request) => {
|
||||
@@ -29,7 +33,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
return ATCookie;
|
||||
},
|
||||
]),
|
||||
secretOrKey: process.env.JWT_SECRET,
|
||||
secretOrKey: configService.get('JWT_SECRET'),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,19 +5,21 @@ import { AuthService } from '../auth.service';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
import * as O from 'fp-ts/Option';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class MicrosoftStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private usersService: UserService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
super({
|
||||
clientID: process.env.MICROSOFT_CLIENT_ID,
|
||||
clientSecret: process.env.MICROSOFT_CLIENT_SECRET,
|
||||
callbackURL: process.env.MICROSOFT_CALLBACK_URL,
|
||||
scope: [process.env.MICROSOFT_SCOPE],
|
||||
tenant: process.env.MICROSOFT_TENANT,
|
||||
clientID: configService.get('INFRA.MICROSOFT_CLIENT_ID'),
|
||||
clientSecret: configService.get('INFRA.MICROSOFT_CLIENT_SECRET'),
|
||||
callbackURL: configService.get('MICROSOFT_CALLBACK_URL'),
|
||||
scope: [configService.get('MICROSOFT_SCOPE')],
|
||||
tenant: configService.get('MICROSOFT_TENANT'),
|
||||
store: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,10 +14,14 @@ import {
|
||||
USER_NOT_FOUND,
|
||||
} from 'src/errors';
|
||||
import * as O from 'fp-ts/Option';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class RTJwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
|
||||
constructor(private usersService: UserService) {
|
||||
constructor(
|
||||
private usersService: UserService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromExtractors([
|
||||
(request: Request) => {
|
||||
@@ -28,7 +32,7 @@ export class RTJwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
|
||||
return RTCookie;
|
||||
},
|
||||
]),
|
||||
secretOrKey: process.env.JWT_SECRET,
|
||||
secretOrKey: configService.get('JWT_SECRET'),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -644,3 +644,41 @@ export const SHORTCODE_INVALID_PROPERTIES_JSON =
|
||||
*/
|
||||
export const SHORTCODE_PROPERTIES_NOT_FOUND =
|
||||
'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';
|
||||
|
||||
44
packages/hoppscotch-backend/src/infra-config/helper.ts
Normal file
44
packages/hoppscotch-backend/src/infra-config/helper.ts
Normal 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);
|
||||
}
|
||||
@@ -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',
|
||||
});
|
||||
@@ -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 {}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
30
packages/hoppscotch-backend/src/infra-config/input-args.ts
Normal file
30
packages/hoppscotch-backend/src/infra-config/input-args.ts
Normal 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;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { MailerModule as NestMailerModule } from '@nestjs-modules/mailer';
|
||||
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
|
||||
import { MailerService } from './mailer.service';
|
||||
@@ -7,24 +7,42 @@ import {
|
||||
MAILER_FROM_ADDRESS_UNDEFINED,
|
||||
MAILER_SMTP_URL_UNDEFINED,
|
||||
} from 'src/errors';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { loadInfraConfiguration } from 'src/infra-config/helper';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
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(),
|
||||
},
|
||||
}),
|
||||
],
|
||||
imports: [],
|
||||
providers: [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(),
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,18 +6,23 @@ import { VersioningType } from '@nestjs/common';
|
||||
import * as session from 'express-session';
|
||||
import { emitGQLSchemaFile } from './gql-schema';
|
||||
import { checkEnvironmentAuthProvider } from './utils';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
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 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(
|
||||
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');
|
||||
|
||||
app.enableCors({
|
||||
origin: process.env.WHITELISTED_ORIGINS.split(','),
|
||||
origin: configService.get('WHITELISTED_ORIGINS').split(','),
|
||||
credentials: true,
|
||||
});
|
||||
} else {
|
||||
console.log('Enabling CORS with production settings');
|
||||
|
||||
app.enableCors({
|
||||
origin: process.env.WHITELISTED_ORIGINS.split(','),
|
||||
origin: configService.get('WHITELISTED_ORIGINS').split(','),
|
||||
credentials: true,
|
||||
});
|
||||
}
|
||||
@@ -47,7 +52,13 @@ async function bootstrap() {
|
||||
type: VersioningType.URI,
|
||||
});
|
||||
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) {
|
||||
|
||||
@@ -504,20 +504,24 @@ describe('ShortcodeService', () => {
|
||||
);
|
||||
expect(result).toEqual(<ShortcodeWithUserEmail[]>[
|
||||
{
|
||||
id: shortcodes[0].id,
|
||||
request: JSON.stringify(shortcodes[0].request),
|
||||
properties: JSON.stringify(shortcodes[0].embedProperties),
|
||||
createdOn: shortcodes[0].createdOn,
|
||||
id: shortcodesWithUserEmail[0].id,
|
||||
request: JSON.stringify(shortcodesWithUserEmail[0].request),
|
||||
properties: JSON.stringify(
|
||||
shortcodesWithUserEmail[0].embedProperties,
|
||||
),
|
||||
createdOn: shortcodesWithUserEmail[0].createdOn,
|
||||
creator: {
|
||||
uid: user.uid,
|
||||
email: user.email,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: shortcodes[1].id,
|
||||
request: JSON.stringify(shortcodes[1].request),
|
||||
properties: JSON.stringify(shortcodes[1].embedProperties),
|
||||
createdOn: shortcodes[1].createdOn,
|
||||
id: shortcodesWithUserEmail[1].id,
|
||||
request: JSON.stringify(shortcodesWithUserEmail[1].request),
|
||||
properties: JSON.stringify(
|
||||
shortcodesWithUserEmail[1].embedProperties,
|
||||
),
|
||||
createdOn: shortcodesWithUserEmail[1].createdOn,
|
||||
creator: {
|
||||
uid: user.uid,
|
||||
email: user.email,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MailerModule } from 'src/mailer/mailer.module';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { PubSubModule } from 'src/pubsub/pubsub.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';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, TeamModule, PubSubModule, UserModule, MailerModule],
|
||||
imports: [PrismaModule, TeamModule, PubSubModule, UserModule],
|
||||
providers: [
|
||||
TeamInvitationService,
|
||||
TeamInvitationResolver,
|
||||
|
||||
@@ -20,6 +20,7 @@ import { UserService } from 'src/user/user.service';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
import { validateEmail } from '../utils';
|
||||
import { AuthUser } from 'src/types/AuthUser';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class TeamInvitationService {
|
||||
@@ -28,8 +29,8 @@ export class TeamInvitationService {
|
||||
private readonly userService: UserService,
|
||||
private readonly teamService: TeamService,
|
||||
private readonly mailerService: MailerService,
|
||||
|
||||
private readonly pubsub: PubSubService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -150,7 +151,9 @@ export class TeamInvitationService {
|
||||
template: 'team-invitation',
|
||||
variables: {
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
29
packages/hoppscotch-backend/src/types/InfraConfig.ts
Normal file
29
packages/hoppscotch-backend/src/types/InfraConfig.ts
Normal 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',
|
||||
}
|
||||
@@ -131,6 +131,26 @@ export const validateEmail = (email: string) => {
|
||||
).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
|
||||
* @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
|
||||
* 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.
|
||||
*/
|
||||
export function checkEnvironmentAuthProvider() {
|
||||
if (!process.env.hasOwnProperty('VITE_ALLOWED_AUTH_PROVIDERS')) {
|
||||
export function checkEnvironmentAuthProvider(
|
||||
VITE_ALLOWED_AUTH_PROVIDERS: string,
|
||||
) {
|
||||
if (!VITE_ALLOWED_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);
|
||||
}
|
||||
|
||||
const givenAuthProviders = process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(
|
||||
',',
|
||||
).map((provider) => provider.toLocaleUpperCase());
|
||||
const givenAuthProviders = VITE_ALLOWED_AUTH_PROVIDERS.split(',').map(
|
||||
(provider) => provider.toLocaleUpperCase(),
|
||||
);
|
||||
const supportedAuthProviders = Object.values(AuthProvider).map(
|
||||
(provider: string) => provider.toLocaleUpperCase(),
|
||||
);
|
||||
|
||||
1166
pnpm-lock.yaml
generated
1166
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user