Compare commits
29 Commits
experiment
...
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
|
# 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: .
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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[] = [
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
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 { 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(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
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);
|
).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
1166
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user