Compare commits
9 Commits
refactor/w
...
feat/invit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ec4714afa | ||
|
|
da2ec5a46d | ||
|
|
ab87a7a16b | ||
|
|
8b14f77d78 | ||
|
|
bcaed7ec69 | ||
|
|
c371b56a23 | ||
|
|
5f52acacc0 | ||
|
|
742eca6d10 | ||
|
|
1e860c3535 |
13
.env.example
13
.env.example
@@ -35,9 +35,20 @@ MICROSOFT_SCOPE="user.read"
|
|||||||
MICROSOFT_TENANT="common"
|
MICROSOFT_TENANT="common"
|
||||||
|
|
||||||
# Mailer config
|
# Mailer config
|
||||||
MAILER_SMTP_URL="smtps://user@domain.com:pass@smtp.domain.com"
|
MAILER_SMTP_ENABLE="true"
|
||||||
|
MAILER_USE_ADVANCE_CONFIGS="false"
|
||||||
MAILER_ADDRESS_FROM='"From Name Here" <from@example.com>'
|
MAILER_ADDRESS_FROM='"From Name Here" <from@example.com>'
|
||||||
|
|
||||||
|
MAILER_SMTP_URL="smtps://user@domain.com:pass@smtp.domain.com" # used if custom mailer configs is false
|
||||||
|
|
||||||
|
# The following are used if custom mailer configs is true
|
||||||
|
MAILER_SMTP_HOST="smtp.domain.com"
|
||||||
|
MAILER_SMTP_PORT="587"
|
||||||
|
MAILER_SMTP_SECURE="true"
|
||||||
|
MAILER_SMTP_USER="user@domain.com"
|
||||||
|
MAILER_SMTP_PASSWORD="pass"
|
||||||
|
MAILER_TLS_REJECT_UNAUTHORIZED="true"
|
||||||
|
|
||||||
# Rate Limit Config
|
# Rate Limit Config
|
||||||
RATE_LIMIT_TTL=60 # In seconds
|
RATE_LIMIT_TTL=60 # In seconds
|
||||||
RATE_LIMIT_MAX=100 # Max requests per IP
|
RATE_LIMIT_MAX=100 # Max requests per IP
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"@nestjs/common": "10.2.7",
|
"@nestjs/common": "10.2.7",
|
||||||
"@nestjs/config": "3.1.1",
|
"@nestjs/config": "3.1.1",
|
||||||
"@nestjs/core": "10.2.7",
|
"@nestjs/core": "10.2.7",
|
||||||
|
"@nestjs/event-emitter": "2.0.4",
|
||||||
"@nestjs/graphql": "12.0.9",
|
"@nestjs/graphql": "12.0.9",
|
||||||
"@nestjs/jwt": "10.1.1",
|
"@nestjs/jwt": "10.1.1",
|
||||||
"@nestjs/passport": "10.0.2",
|
"@nestjs/passport": "10.0.2",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
|||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { OffsetPaginationArgs } from 'src/types/input-types.args';
|
import { OffsetPaginationArgs } from 'src/types/input-types.args';
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
const mockPrisma = mockDeep<PrismaService>();
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
const mockPubSub = mockDeep<PubSubService>();
|
const mockPubSub = mockDeep<PubSubService>();
|
||||||
@@ -34,6 +35,7 @@ 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 mockConfigService = mockDeep<ConfigService>();
|
||||||
|
const mockEventEmitter = mockDeep<EventEmitter2>();
|
||||||
|
|
||||||
const adminService = new AdminService(
|
const adminService = new AdminService(
|
||||||
mockUserService,
|
mockUserService,
|
||||||
@@ -44,9 +46,9 @@ const adminService = new AdminService(
|
|||||||
mockTeamInvitationService,
|
mockTeamInvitationService,
|
||||||
mockPubSub as any,
|
mockPubSub as any,
|
||||||
mockPrisma as any,
|
mockPrisma as any,
|
||||||
mockMailerService,
|
|
||||||
mockShortcodeService,
|
mockShortcodeService,
|
||||||
mockConfigService,
|
mockConfigService,
|
||||||
|
mockEventEmitter,
|
||||||
);
|
);
|
||||||
|
|
||||||
const invitedUsers: InvitedUsers[] = [
|
const invitedUsers: InvitedUsers[] = [
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
USER_IS_ADMIN,
|
USER_IS_ADMIN,
|
||||||
USER_NOT_FOUND,
|
USER_NOT_FOUND,
|
||||||
} from '../errors';
|
} from '../errors';
|
||||||
import { MailerService } from '../mailer/mailer.service';
|
|
||||||
import { InvitedUser } from './invited-user.model';
|
import { InvitedUser } from './invited-user.model';
|
||||||
import { TeamService } from '../team/team.service';
|
import { TeamService } from '../team/team.service';
|
||||||
import { TeamCollectionService } from '../team-collection/team-collection.service';
|
import { TeamCollectionService } from '../team-collection/team-collection.service';
|
||||||
@@ -31,6 +30,8 @@ import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
|||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { OffsetPaginationArgs } from 'src/types/input-types.args';
|
import { OffsetPaginationArgs } from 'src/types/input-types.args';
|
||||||
import { UserDeletionResult } from 'src/user/user.model';
|
import { UserDeletionResult } from 'src/user/user.model';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { Events } from 'src/types/EventEmitter';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AdminService {
|
export class AdminService {
|
||||||
@@ -43,9 +44,9 @@ export class AdminService {
|
|||||||
private readonly teamInvitationService: TeamInvitationService,
|
private readonly teamInvitationService: TeamInvitationService,
|
||||||
private readonly pubsub: PubSubService,
|
private readonly pubsub: PubSubService,
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly mailerService: MailerService,
|
|
||||||
private readonly shortcodeService: ShortcodeService,
|
private readonly shortcodeService: ShortcodeService,
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
|
private readonly eventEmitter: EventEmitter2,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,17 +100,16 @@ export class AdminService {
|
|||||||
});
|
});
|
||||||
if (alreadyInvitedUser != null) return E.left(USER_ALREADY_INVITED);
|
if (alreadyInvitedUser != null) return E.left(USER_ALREADY_INVITED);
|
||||||
|
|
||||||
try {
|
this.eventEmitter.emit(Events.MAILER_SEND_USER_INVITATION_EMAIL, {
|
||||||
await this.mailerService.sendUserInvitationEmail(inviteeEmail, {
|
to: inviteeEmail,
|
||||||
|
mailDesc: {
|
||||||
template: 'user-invitation',
|
template: 'user-invitation',
|
||||||
variables: {
|
variables: {
|
||||||
inviteeEmail: inviteeEmail,
|
inviteeEmail: inviteeEmail,
|
||||||
magicLink: `${this.configService.get('VITE_BASE_URL')}`,
|
magicLink: `${this.configService.get('VITE_BASE_URL')}`,
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
} catch (e) {
|
});
|
||||||
return E.left(EMAIL_FAILED);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add invitee email to the list of invited users by admin
|
// Add invitee email to the list of invited users by admin
|
||||||
const dbInvitedUser = await this.prisma.invitedUsers.create({
|
const dbInvitedUser = await this.prisma.invitedUsers.create({
|
||||||
|
|||||||
@@ -292,6 +292,14 @@ export class InfraResolver {
|
|||||||
return this.infraConfigService.getAllowedAuthProviders();
|
return this.infraConfigService.getAllowedAuthProviders();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Query(() => Boolean, {
|
||||||
|
description: 'Check if the SMTP is enabled or not',
|
||||||
|
})
|
||||||
|
@UseGuards(GqlAuthGuard)
|
||||||
|
isSMTPEnabled() {
|
||||||
|
return this.infraConfigService.isSMTPEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
/* Mutations */
|
/* Mutations */
|
||||||
|
|
||||||
@Mutation(() => [InfraConfig], {
|
@Mutation(() => [InfraConfig], {
|
||||||
@@ -359,4 +367,23 @@ export class InfraResolver {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Mutation(() => Boolean, {
|
||||||
|
description: 'Enable or Disable SMTP for sending emails',
|
||||||
|
})
|
||||||
|
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||||
|
async toggleSMTP(
|
||||||
|
@Args({
|
||||||
|
name: 'status',
|
||||||
|
type: () => ServiceStatus,
|
||||||
|
description: 'Toggle SMTP',
|
||||||
|
})
|
||||||
|
status: ServiceStatus,
|
||||||
|
) {
|
||||||
|
const isUpdated = await this.infraConfigService.enableAndDisableSMTP(
|
||||||
|
status,
|
||||||
|
);
|
||||||
|
if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,11 @@ import { MailerModule } from './mailer/mailer.module';
|
|||||||
import { PosthogModule } from './posthog/posthog.module';
|
import { PosthogModule } from './posthog/posthog.module';
|
||||||
import { ScheduleModule } from '@nestjs/schedule';
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
import { HealthModule } from './health/health.module';
|
import { HealthModule } from './health/health.module';
|
||||||
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
EventEmitterModule.forRoot(),
|
||||||
ConfigModule.forRoot({
|
ConfigModule.forRoot({
|
||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
load: [async () => loadInfraConfiguration()],
|
load: [async () => loadInfraConfiguration()],
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ 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 { ConfigService } from '@nestjs/config';
|
||||||
import { InfraConfigService } from 'src/infra-config/infra-config.service';
|
import { InfraConfigService } from 'src/infra-config/infra-config.service';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
const mockPrisma = mockDeep<PrismaService>();
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
const mockUser = mockDeep<UserService>();
|
const mockUser = mockDeep<UserService>();
|
||||||
@@ -30,6 +31,7 @@ const mockJWT = mockDeep<JwtService>();
|
|||||||
const mockMailer = mockDeep<MailerService>();
|
const mockMailer = mockDeep<MailerService>();
|
||||||
const mockConfigService = mockDeep<ConfigService>();
|
const mockConfigService = mockDeep<ConfigService>();
|
||||||
const mockInfraConfigService = mockDeep<InfraConfigService>();
|
const mockInfraConfigService = mockDeep<InfraConfigService>();
|
||||||
|
const mockEventEmitter = mockDeep<EventEmitter2>();
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -37,9 +39,9 @@ const authService = new AuthService(
|
|||||||
mockUser,
|
mockUser,
|
||||||
mockPrisma,
|
mockPrisma,
|
||||||
mockJWT,
|
mockJWT,
|
||||||
mockMailer,
|
|
||||||
mockConfigService,
|
mockConfigService,
|
||||||
mockInfraConfigService,
|
mockInfraConfigService,
|
||||||
|
mockEventEmitter,
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentTime = new Date();
|
const currentTime = new Date();
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { HttpStatus, Injectable } from '@nestjs/common';
|
import { HttpStatus, Injectable } from '@nestjs/common';
|
||||||
import { MailerService } from 'src/mailer/mailer.service';
|
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { UserService } from 'src/user/user.service';
|
import { UserService } from 'src/user/user.service';
|
||||||
import { VerifyMagicDto } from './dto/verify-magic.dto';
|
import { VerifyMagicDto } from './dto/verify-magic.dto';
|
||||||
@@ -30,6 +29,8 @@ import { VerificationToken } from '@prisma/client';
|
|||||||
import { Origin } from './helper';
|
import { Origin } from './helper';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { InfraConfigService } from 'src/infra-config/infra-config.service';
|
import { InfraConfigService } from 'src/infra-config/infra-config.service';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { Events } from 'src/types/EventEmitter';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
@@ -37,9 +38,9 @@ export class AuthService {
|
|||||||
private usersService: UserService,
|
private usersService: UserService,
|
||||||
private prismaService: PrismaService,
|
private prismaService: PrismaService,
|
||||||
private jwtService: JwtService,
|
private jwtService: JwtService,
|
||||||
private readonly mailerService: MailerService,
|
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
private infraConfigService: InfraConfigService,
|
private infraConfigService: InfraConfigService,
|
||||||
|
private eventEmitter: EventEmitter2,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -234,11 +235,14 @@ export class AuthService {
|
|||||||
url = this.configService.get('VITE_BASE_URL');
|
url = this.configService.get('VITE_BASE_URL');
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.mailerService.sendEmail(email, {
|
this.eventEmitter.emit(Events.MAILER_SEND_EMAIL, {
|
||||||
template: 'user-invitation',
|
to: email,
|
||||||
variables: {
|
mailDesc: {
|
||||||
inviteeEmail: email,
|
template: 'user-invitation',
|
||||||
magicLink: `${url}/enter?token=${generatedTokens.token}`,
|
variables: {
|
||||||
|
inviteeEmail: email,
|
||||||
|
magicLink: `${url}/enter?token=${generatedTokens.token}`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -678,6 +678,19 @@ export const MAILER_SMTP_URL_UNDEFINED = 'mailer/smtp_url_undefined' as const;
|
|||||||
export const MAILER_FROM_ADDRESS_UNDEFINED =
|
export const MAILER_FROM_ADDRESS_UNDEFINED =
|
||||||
'mailer/from_address_undefined' as const;
|
'mailer/from_address_undefined' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MAILER_SMTP_USER environment variable is not defined
|
||||||
|
* (MailerModule)
|
||||||
|
*/
|
||||||
|
export const MAILER_SMTP_USER_UNDEFINED = 'mailer/smtp_user_undefined' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MAILER_SMTP_PASSWORD environment variable is not defined
|
||||||
|
* (MailerModule)
|
||||||
|
*/
|
||||||
|
export const MAILER_SMTP_PASSWORD_UNDEFINED =
|
||||||
|
'mailer/smtp_password_undefined' as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SharedRequest invalid request JSON format
|
* SharedRequest invalid request JSON format
|
||||||
* (ShortcodeService)
|
* (ShortcodeService)
|
||||||
|
|||||||
@@ -33,10 +33,17 @@ const AuthProviderConfigurations = {
|
|||||||
InfraConfigEnum.MICROSOFT_SCOPE,
|
InfraConfigEnum.MICROSOFT_SCOPE,
|
||||||
InfraConfigEnum.MICROSOFT_TENANT,
|
InfraConfigEnum.MICROSOFT_TENANT,
|
||||||
],
|
],
|
||||||
[AuthProvider.EMAIL]: [
|
[AuthProvider.EMAIL]: !!process.env.MAILER_USE_CUSTOM_CONFIGS
|
||||||
InfraConfigEnum.MAILER_SMTP_URL,
|
? [
|
||||||
InfraConfigEnum.MAILER_ADDRESS_FROM,
|
InfraConfigEnum.MAILER_SMTP_HOST,
|
||||||
],
|
InfraConfigEnum.MAILER_SMTP_PORT,
|
||||||
|
InfraConfigEnum.MAILER_SMTP_SECURE,
|
||||||
|
InfraConfigEnum.MAILER_SMTP_USER,
|
||||||
|
InfraConfigEnum.MAILER_SMTP_PASSWORD,
|
||||||
|
InfraConfigEnum.MAILER_TLS_REJECT_UNAUTHORIZED,
|
||||||
|
InfraConfigEnum.MAILER_ADDRESS_FROM,
|
||||||
|
]
|
||||||
|
: [InfraConfigEnum.MAILER_SMTP_URL, InfraConfigEnum.MAILER_ADDRESS_FROM],
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,6 +82,14 @@ export async function getDefaultInfraConfigs(): Promise<
|
|||||||
|
|
||||||
// Prepare rows for 'infra_config' table with default values (from .env) for each 'name'
|
// Prepare rows for 'infra_config' table with default values (from .env) for each 'name'
|
||||||
const infraConfigDefaultObjs: { name: InfraConfigEnum; value: string }[] = [
|
const infraConfigDefaultObjs: { name: InfraConfigEnum; value: string }[] = [
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MAILER_SMTP_ENABLE,
|
||||||
|
value: process.env.MAILER_SMTP_ENABLE ?? 'true',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MAILER_USE_CUSTOM_CONFIGS,
|
||||||
|
value: process.env.MAILER_USE_CUSTOM_CONFIGS ?? 'false',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.MAILER_SMTP_URL,
|
name: InfraConfigEnum.MAILER_SMTP_URL,
|
||||||
value: process.env.MAILER_SMTP_URL,
|
value: process.env.MAILER_SMTP_URL,
|
||||||
@@ -83,6 +98,30 @@ export async function getDefaultInfraConfigs(): Promise<
|
|||||||
name: InfraConfigEnum.MAILER_ADDRESS_FROM,
|
name: InfraConfigEnum.MAILER_ADDRESS_FROM,
|
||||||
value: process.env.MAILER_ADDRESS_FROM,
|
value: process.env.MAILER_ADDRESS_FROM,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MAILER_SMTP_HOST,
|
||||||
|
value: process.env.MAILER_SMTP_HOST,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MAILER_SMTP_PORT,
|
||||||
|
value: process.env.MAILER_SMTP_PORT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MAILER_SMTP_SECURE,
|
||||||
|
value: process.env.MAILER_SMTP_SECURE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MAILER_SMTP_USER,
|
||||||
|
value: process.env.MAILER_SMTP_USER,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MAILER_SMTP_PASSWORD,
|
||||||
|
value: process.env.MAILER_SMTP_PASSWORD,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MAILER_TLS_REJECT_UNAUTHORIZED,
|
||||||
|
value: process.env.MAILER_TLS_REJECT_UNAUTHORIZED,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: InfraConfigEnum.GOOGLE_CLIENT_ID,
|
name: InfraConfigEnum.GOOGLE_CLIENT_ID,
|
||||||
value: process.env.GOOGLE_CLIENT_ID,
|
value: process.env.GOOGLE_CLIENT_ID,
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
|
InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
|
||||||
InfraConfigEnum.ANALYTICS_USER_ID,
|
InfraConfigEnum.ANALYTICS_USER_ID,
|
||||||
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
||||||
|
InfraConfigEnum.MAILER_SMTP_ENABLE,
|
||||||
];
|
];
|
||||||
// Following fields can not be fetched by `infraConfigs` Query. Use dedicated queries for these fields instead.
|
// Following fields can not be fetched by `infraConfigs` Query. Use dedicated queries for these fields instead.
|
||||||
EXCLUDE_FROM_FETCH_CONFIGS = [
|
EXCLUDE_FROM_FETCH_CONFIGS = [
|
||||||
@@ -218,6 +219,47 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
return E.right(isUpdated.right.value === 'true');
|
return E.right(isUpdated.right.value === 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or Disable SMTP
|
||||||
|
* @param status Status to enable or disable
|
||||||
|
* @returns Either true or an error
|
||||||
|
*/
|
||||||
|
async enableAndDisableSMTP(status: ServiceStatus) {
|
||||||
|
const isUpdated = await this.toggleServiceStatus(
|
||||||
|
InfraConfigEnum.MAILER_SMTP_ENABLE,
|
||||||
|
status,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
if (E.isLeft(isUpdated)) return E.left(isUpdated.left);
|
||||||
|
|
||||||
|
if (status === ServiceStatus.DISABLE) {
|
||||||
|
this.enableAndDisableSSO([{ provider: AuthProvider.EMAIL, status }]);
|
||||||
|
}
|
||||||
|
return E.right(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or Disable Service (i.e. ALLOW_AUDIT_LOGS, ALLOW_ANALYTICS_COLLECTION, ALLOW_DOMAIN_WHITELISTING, SITE_PROTECTION)
|
||||||
|
* @param configName Name of the InfraConfigEnum
|
||||||
|
* @param status Status to enable or disable
|
||||||
|
* @param restartEnabled If true, restart the app after updating the InfraConfig
|
||||||
|
* @returns Either true or an error
|
||||||
|
*/
|
||||||
|
async toggleServiceStatus(
|
||||||
|
configName: InfraConfigEnum,
|
||||||
|
status: ServiceStatus,
|
||||||
|
restartEnabled = false,
|
||||||
|
) {
|
||||||
|
const isUpdated = await this.update(
|
||||||
|
configName,
|
||||||
|
status === ServiceStatus.ENABLE ? 'true' : 'false',
|
||||||
|
restartEnabled,
|
||||||
|
);
|
||||||
|
if (E.isLeft(isUpdated)) return E.left(isUpdated.left);
|
||||||
|
|
||||||
|
return E.right(true);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable or Disable SSO for login/signup
|
* Enable or Disable SSO for login/signup
|
||||||
* @param provider Auth Provider to enable or disable
|
* @param provider Auth Provider to enable or disable
|
||||||
@@ -316,6 +358,16 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
.split(',');
|
.split(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if SMTP is enabled or not
|
||||||
|
* @returns boolean
|
||||||
|
*/
|
||||||
|
isSMTPEnabled() {
|
||||||
|
return (
|
||||||
|
this.configService.get<string>('INFRA.MAILER_SMTP_ENABLE') === 'true'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset all the InfraConfigs to their default values (from .env)
|
* Reset all the InfraConfigs to their default values (from .env)
|
||||||
*/
|
*/
|
||||||
@@ -363,6 +415,20 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
) {
|
) {
|
||||||
for (let i = 0; i < infraConfigs.length; i++) {
|
for (let i = 0; i < infraConfigs.length; i++) {
|
||||||
switch (infraConfigs[i].name) {
|
switch (infraConfigs[i].name) {
|
||||||
|
case InfraConfigEnum.MAILER_SMTP_ENABLE:
|
||||||
|
if (
|
||||||
|
infraConfigs[i].value !== 'true' &&
|
||||||
|
infraConfigs[i].value !== 'false'
|
||||||
|
)
|
||||||
|
return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
break;
|
||||||
|
case InfraConfigEnum.MAILER_USE_CUSTOM_CONFIGS:
|
||||||
|
if (
|
||||||
|
infraConfigs[i].value !== 'true' &&
|
||||||
|
infraConfigs[i].value !== 'false'
|
||||||
|
)
|
||||||
|
return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
break;
|
||||||
case InfraConfigEnum.MAILER_SMTP_URL:
|
case InfraConfigEnum.MAILER_SMTP_URL:
|
||||||
const isValidUrl = validateSMTPUrl(infraConfigs[i].value);
|
const isValidUrl = validateSMTPUrl(infraConfigs[i].value);
|
||||||
if (!isValidUrl) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!isValidUrl) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
@@ -371,6 +437,32 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
const isValidEmail = validateSMTPEmail(infraConfigs[i].value);
|
const isValidEmail = validateSMTPEmail(infraConfigs[i].value);
|
||||||
if (!isValidEmail) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!isValidEmail) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
|
case InfraConfigEnum.MAILER_SMTP_HOST:
|
||||||
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
break;
|
||||||
|
case InfraConfigEnum.MAILER_SMTP_PORT:
|
||||||
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
break;
|
||||||
|
case InfraConfigEnum.MAILER_SMTP_SECURE:
|
||||||
|
if (
|
||||||
|
infraConfigs[i].value !== 'true' &&
|
||||||
|
infraConfigs[i].value !== 'false'
|
||||||
|
)
|
||||||
|
return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
break;
|
||||||
|
case InfraConfigEnum.MAILER_SMTP_USER:
|
||||||
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
break;
|
||||||
|
case InfraConfigEnum.MAILER_SMTP_PASSWORD:
|
||||||
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
break;
|
||||||
|
case InfraConfigEnum.MAILER_TLS_REJECT_UNAUTHORIZED:
|
||||||
|
if (
|
||||||
|
infraConfigs[i].value !== 'true' &&
|
||||||
|
infraConfigs[i].value !== 'false'
|
||||||
|
)
|
||||||
|
return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
break;
|
||||||
case InfraConfigEnum.GOOGLE_CLIENT_ID:
|
case InfraConfigEnum.GOOGLE_CLIENT_ID:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
|
|||||||
30
packages/hoppscotch-backend/src/mailer/mailer.listener.ts
Normal file
30
packages/hoppscotch-backend/src/mailer/mailer.listener.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
import { Events } from 'src/types/EventEmitter';
|
||||||
|
import {
|
||||||
|
AdminUserInvitationMailDescription,
|
||||||
|
MailDescription,
|
||||||
|
UserMagicLinkMailDescription,
|
||||||
|
} from './MailDescriptions';
|
||||||
|
import { MailerService } from './mailer.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MailerEventListener {
|
||||||
|
constructor(private mailerService: MailerService) {}
|
||||||
|
|
||||||
|
@OnEvent(Events.MAILER_SEND_EMAIL, { async: true })
|
||||||
|
async handleSendEmailEvent(data: {
|
||||||
|
to: string;
|
||||||
|
mailDesc: MailDescription | UserMagicLinkMailDescription;
|
||||||
|
}) {
|
||||||
|
await this.mailerService.sendEmail(data.to, data.mailDesc);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent(Events.MAILER_SEND_USER_INVITATION_EMAIL, { async: true })
|
||||||
|
async handleSendUserInvitationEmailEvent(data: {
|
||||||
|
to: string;
|
||||||
|
mailDesc: AdminUserInvitationMailDescription;
|
||||||
|
}) {
|
||||||
|
await this.mailerService.sendUserInvitationEmail(data.to, data.mailDesc);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,38 +4,43 @@ import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handleba
|
|||||||
import { MailerService } from './mailer.service';
|
import { MailerService } from './mailer.service';
|
||||||
import { throwErr } from 'src/utils';
|
import { throwErr } from 'src/utils';
|
||||||
import {
|
import {
|
||||||
MAILER_FROM_ADDRESS_UNDEFINED,
|
MAILER_SMTP_PASSWORD_UNDEFINED,
|
||||||
MAILER_SMTP_URL_UNDEFINED,
|
MAILER_SMTP_URL_UNDEFINED,
|
||||||
|
MAILER_SMTP_USER_UNDEFINED,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { loadInfraConfiguration } from 'src/infra-config/helper';
|
import { loadInfraConfiguration } from 'src/infra-config/helper';
|
||||||
|
import { MailerEventListener } from './mailer.listener';
|
||||||
|
import { TransportType } from '@nestjs-modules/mailer/dist/interfaces/mailer-options.interface';
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({})
|
||||||
imports: [],
|
|
||||||
providers: [MailerService],
|
|
||||||
exports: [MailerService],
|
|
||||||
})
|
|
||||||
export class MailerModule {
|
export class MailerModule {
|
||||||
static async register() {
|
static async register() {
|
||||||
|
const config = new ConfigService();
|
||||||
const env = await loadInfraConfiguration();
|
const env = await loadInfraConfiguration();
|
||||||
|
|
||||||
let mailerSmtpUrl = env.INFRA.MAILER_SMTP_URL;
|
// If mailer SMTP is DISABLED, return the module without any configuration (service, listener, etc.)
|
||||||
let mailerAddressFrom = env.INFRA.MAILER_ADDRESS_FROM;
|
if (env.INFRA.MAILER_SMTP_ENABLE !== 'true') {
|
||||||
|
console.log('Mailer SMTP is disabled');
|
||||||
if (!env.INFRA.MAILER_SMTP_URL || !env.INFRA.MAILER_ADDRESS_FROM) {
|
return { module: MailerModule };
|
||||||
const config = new ConfigService();
|
|
||||||
mailerSmtpUrl = config.get('MAILER_SMTP_URL');
|
|
||||||
mailerAddressFrom = config.get('MAILER_ADDRESS_FROM');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If mailer is ENABLED, return the module with configuration (service, listener, etc.)
|
||||||
|
|
||||||
|
// Determine transport configuration based on custom config flag
|
||||||
|
let transportOption = getTransportOption(env, config);
|
||||||
|
// Get mailer address from environment or config
|
||||||
|
const mailerAddressFrom = getMailerAddressFrom(env, config);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
module: MailerModule,
|
module: MailerModule,
|
||||||
|
providers: [MailerService, MailerEventListener],
|
||||||
imports: [
|
imports: [
|
||||||
NestMailerModule.forRoot({
|
NestMailerModule.forRoot({
|
||||||
transport: mailerSmtpUrl ?? throwErr(MAILER_SMTP_URL_UNDEFINED),
|
transport: transportOption,
|
||||||
defaults: {
|
defaults: {
|
||||||
from: mailerAddressFrom ?? throwErr(MAILER_FROM_ADDRESS_UNDEFINED),
|
from: mailerAddressFrom,
|
||||||
},
|
},
|
||||||
template: {
|
template: {
|
||||||
dir: __dirname + '/templates',
|
dir: __dirname + '/templates',
|
||||||
@@ -46,3 +51,52 @@ export class MailerModule {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isEnabled(value) {
|
||||||
|
return value === 'true';
|
||||||
|
}
|
||||||
|
function getMailerAddressFrom(env, config): string {
|
||||||
|
return (
|
||||||
|
env.INFRA.MAILER_ADDRESS_FROM ??
|
||||||
|
config.get('MAILER_ADDRESS_FROM') ??
|
||||||
|
throwErr(MAILER_SMTP_URL_UNDEFINED)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function getTransportOption(env, config): TransportType {
|
||||||
|
const useCustomConfigs = isEnabled(
|
||||||
|
env.INFRA.MAILER_USE_CUSTOM_CONFIGS ??
|
||||||
|
config.get('MAILER_USE_CUSTOM_CONFIGS'),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!useCustomConfigs) {
|
||||||
|
console.log('Using simple mailer configuration');
|
||||||
|
return (
|
||||||
|
env.INFRA.MAILER_SMTP_URL ??
|
||||||
|
config.get('MAILER_SMTP_URL') ??
|
||||||
|
throwErr(MAILER_SMTP_URL_UNDEFINED)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log('Using advanced mailer configuration');
|
||||||
|
return {
|
||||||
|
host: env.INFRA.MAILER_SMTP_HOST ?? config.get('MAILER_SMTP_HOST'),
|
||||||
|
port: +env.INFRA.MAILER_SMTP_PORT ?? +config.get('MAILER_SMTP_PORT'),
|
||||||
|
secure:
|
||||||
|
!!env.INFRA.MAILER_SMTP_SECURE ?? !!config.get('MAILER_SMTP_SECURE'),
|
||||||
|
auth: {
|
||||||
|
user:
|
||||||
|
env.INFRA.MAILER_SMTP_USER ??
|
||||||
|
config.get('MAILER_SMTP_USER') ??
|
||||||
|
throwErr(MAILER_SMTP_USER_UNDEFINED),
|
||||||
|
pass:
|
||||||
|
env.INFRA.MAILER_SMTP_PASSWORD ??
|
||||||
|
config.get('MAILER_SMTP_PASSWORD') ??
|
||||||
|
throwErr(MAILER_SMTP_PASSWORD_UNDEFINED),
|
||||||
|
},
|
||||||
|
tls: {
|
||||||
|
rejectUnauthorized:
|
||||||
|
!!env.INFRA.MAILER_TLS_REJECT_UNAUTHORIZED ??
|
||||||
|
!!config.get('MAILER_TLS_REJECT_UNAUTHORIZED'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,10 +7,14 @@ import {
|
|||||||
import { throwErr } from 'src/utils';
|
import { throwErr } from 'src/utils';
|
||||||
import { EMAIL_FAILED } from 'src/errors';
|
import { EMAIL_FAILED } from 'src/errors';
|
||||||
import { MailerService as NestMailerService } from '@nestjs-modules/mailer';
|
import { MailerService as NestMailerService } from '@nestjs-modules/mailer';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MailerService {
|
export class MailerService {
|
||||||
constructor(private readonly nestMailerService: NestMailerService) {}
|
constructor(
|
||||||
|
private readonly nestMailerService: NestMailerService,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes an input mail description and spits out the Email subject required for it
|
* Takes an input mail description and spits out the Email subject required for it
|
||||||
@@ -42,6 +46,8 @@ export class MailerService {
|
|||||||
to: string,
|
to: string,
|
||||||
mailDesc: MailDescription | UserMagicLinkMailDescription,
|
mailDesc: MailDescription | UserMagicLinkMailDescription,
|
||||||
) {
|
) {
|
||||||
|
if (this.configService.get('INFRA.MAILER_SMTP_ENABLE') !== 'true') return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.nestMailerService.sendMail({
|
await this.nestMailerService.sendMail({
|
||||||
to,
|
to,
|
||||||
@@ -64,6 +70,8 @@ export class MailerService {
|
|||||||
to: string,
|
to: string,
|
||||||
mailDesc: AdminUserInvitationMailDescription,
|
mailDesc: AdminUserInvitationMailDescription,
|
||||||
) {
|
) {
|
||||||
|
if (this.configService.get('INFRA.MAILER_SMTP_ENABLE') !== 'true') return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await this.nestMailerService.sendMail({
|
const res = await this.nestMailerService.sendMail({
|
||||||
to,
|
to,
|
||||||
|
|||||||
@@ -15,12 +15,13 @@ import {
|
|||||||
TEAM_MEMBER_NOT_FOUND,
|
TEAM_MEMBER_NOT_FOUND,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import { TeamInvitation } from './team-invitation.model';
|
import { TeamInvitation } from './team-invitation.model';
|
||||||
import { MailerService } from 'src/mailer/mailer.service';
|
|
||||||
import { UserService } from 'src/user/user.service';
|
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';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { Events } from 'src/types/EventEmitter';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TeamInvitationService {
|
export class TeamInvitationService {
|
||||||
@@ -28,9 +29,9 @@ export class TeamInvitationService {
|
|||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly userService: UserService,
|
private readonly userService: UserService,
|
||||||
private readonly teamService: TeamService,
|
private readonly teamService: TeamService,
|
||||||
private readonly mailerService: MailerService,
|
|
||||||
private readonly pubsub: PubSubService,
|
private readonly pubsub: PubSubService,
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
|
private eventEmitter: EventEmitter2,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -147,14 +148,17 @@ export class TeamInvitationService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.mailerService.sendEmail(inviteeEmail, {
|
this.eventEmitter.emit(Events.MAILER_SEND_EMAIL, {
|
||||||
template: 'team-invitation',
|
to: inviteeEmail,
|
||||||
variables: {
|
mailDesc: {
|
||||||
invitee: creator.displayName ?? 'A Hoppscotch User',
|
template: 'team-invitation',
|
||||||
action_url: `${this.configService.get('VITE_BASE_URL')}/join-team?id=${
|
variables: {
|
||||||
dbInvitation.id
|
invitee: creator.displayName ?? 'A Hoppscotch User',
|
||||||
}`,
|
action_url: `${this.configService.get(
|
||||||
invite_team_name: team.name,
|
'VITE_BASE_URL',
|
||||||
|
)}/join-team?id=${dbInvitation.id}`,
|
||||||
|
invite_team_name: team.name,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
4
packages/hoppscotch-backend/src/types/EventEmitter.ts
Normal file
4
packages/hoppscotch-backend/src/types/EventEmitter.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export enum Events {
|
||||||
|
MAILER_SEND_EMAIL = 'mailer.sendEmail',
|
||||||
|
MAILER_SEND_USER_INVITATION_EMAIL = 'mailer.sendUserInvitationEmail',
|
||||||
|
}
|
||||||
@@ -1,7 +1,16 @@
|
|||||||
export enum InfraConfigEnum {
|
export enum InfraConfigEnum {
|
||||||
|
MAILER_SMTP_ENABLE = 'MAILER_SMTP_ENABLE',
|
||||||
|
MAILER_USE_CUSTOM_CONFIGS = 'MAILER_USE_CUSTOM_CONFIGS',
|
||||||
MAILER_SMTP_URL = 'MAILER_SMTP_URL',
|
MAILER_SMTP_URL = 'MAILER_SMTP_URL',
|
||||||
MAILER_ADDRESS_FROM = 'MAILER_ADDRESS_FROM',
|
MAILER_ADDRESS_FROM = 'MAILER_ADDRESS_FROM',
|
||||||
|
|
||||||
|
MAILER_SMTP_HOST = 'MAILER_SMTP_HOST',
|
||||||
|
MAILER_SMTP_PORT = 'MAILER_SMTP_PORT',
|
||||||
|
MAILER_SMTP_SECURE = 'MAILER_SMTP_SECURE',
|
||||||
|
MAILER_SMTP_USER = 'MAILER_SMTP_USER',
|
||||||
|
MAILER_SMTP_PASSWORD = 'MAILER_SMTP_PASSWORD',
|
||||||
|
MAILER_TLS_REJECT_UNAUTHORIZED = 'MAILER_TLS_REJECT_UNAUTHORIZED',
|
||||||
|
|
||||||
GOOGLE_CLIENT_ID = 'GOOGLE_CLIENT_ID',
|
GOOGLE_CLIENT_ID = 'GOOGLE_CLIENT_ID',
|
||||||
GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET',
|
GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET',
|
||||||
GOOGLE_CALLBACK_URL = 'GOOGLE_CALLBACK_URL',
|
GOOGLE_CALLBACK_URL = 'GOOGLE_CALLBACK_URL',
|
||||||
|
|||||||
34235
pnpm-lock.yaml
generated
34235
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user