diff --git a/enterprise/packages b/enterprise/packages
new file mode 160000
index 00000000000..8f0b31fcf98
--- /dev/null
+++ b/enterprise/packages
@@ -0,0 +1 @@
+Subproject commit 8f0b31fcf987bd0626b80224d73349fcbf71ceea
diff --git a/libs/dal/src/dal.service.ts b/libs/dal/src/dal.service.ts
index 93340133b8e..428bb765341 100644
--- a/libs/dal/src/dal.service.ts
+++ b/libs/dal/src/dal.service.ts
@@ -7,8 +7,9 @@ export class DalService {
async connect(url: string, config: ConnectOptions = {}) {
const baseConfig: ConnectOptions = {
maxPoolSize: process.env.MONGO_MAX_POOL_SIZE || 500,
- minPoolSize: process.env.NODE_ENV === 'production' ? 200 : 10,
+ minPoolSize: process.env.NODE_ENV === 'production' ? 50 : 10,
autoIndex: process.env.AUTO_CREATE_INDEXES === 'true',
+ maxIdleTimeMS: 1000 * 60 * 10,
};
const instance = await mongoose.connect(url, {
diff --git a/libs/shared/src/config/job-queue.ts b/libs/shared/src/config/job-queue.ts
index 50ec627998f..826b82f5f0f 100644
--- a/libs/shared/src/config/job-queue.ts
+++ b/libs/shared/src/config/job-queue.ts
@@ -9,4 +9,5 @@ export enum JobTopicNameEnum {
export enum ObservabilityBackgroundTransactionEnum {
JOB_PROCESSING_QUEUE = 'job-processing-queue',
TRIGGER_HANDLER_QUEUE = 'trigger-handler-queue',
+ WS_SOCKET_QUEUE = 'ws_socket_queue',
}
diff --git a/packages/application-generic/package.json b/packages/application-generic/package.json
index 60f9a14847a..7629b5c35d8 100644
--- a/packages/application-generic/package.json
+++ b/packages/application-generic/package.json
@@ -38,6 +38,7 @@
"@nestjs/swagger": ">=6",
"@nestjs/terminus": ">=10",
"@nestjs/testing": ">=10",
+ "@nestjs/jwt": "^10.1.0",
"newrelic": "^9",
"reflect-metadata": "^0.1.13"
},
diff --git a/packages/application-generic/src/index.ts b/packages/application-generic/src/index.ts
index cb1c85709ae..86e982129dd 100644
--- a/packages/application-generic/src/index.ts
+++ b/packages/application-generic/src/index.ts
@@ -12,3 +12,5 @@ export * from './utils/subscriber';
export * from './utils/filter';
export * from './utils/filter-processing-details';
export * from './resilience';
+export * from './utils/exceptions';
+export * from './utils/email-normalization';
diff --git a/apps/api/src/app/auth/services/auth.service.ts b/packages/application-generic/src/services/auth/auth.service.ts
similarity index 65%
rename from apps/api/src/app/auth/services/auth.service.ts
rename to packages/application-generic/src/services/auth/auth.service.ts
index e33cd6d0fb5..4f8c9391d17 100644
--- a/apps/api/src/app/auth/services/auth.service.ts
+++ b/packages/application-generic/src/services/auth/auth.service.ts
@@ -1,5 +1,12 @@
-import { forwardRef, Inject, Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common';
+import {
+ forwardRef,
+ Inject,
+ Injectable,
+ NotFoundException,
+ UnauthorizedException,
+} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
+
import {
EnvironmentRepository,
MemberEntity,
@@ -12,26 +19,35 @@ import {
EnvironmentEntity,
IApiKey,
} from '@novu/dal';
-import { AuthProviderEnum, IJwtPayload, ISubscriberJwt, MemberRoleEnum, SignUpOriginEnum } from '@novu/shared';
import {
- AnalyticsService,
- Instrument,
- PinoLogger,
- CachedEntity,
+ AuthProviderEnum,
+ IJwtPayload,
+ ISubscriberJwt,
+ MemberRoleEnum,
+ SignUpOriginEnum,
+} from '@novu/shared';
+
+import { PinoLogger } from '../../logging';
+import { AnalyticsService } from '../analytics.service';
+import { ApiException } from '../../utils/exceptions';
+import { Instrument } from '../../instrumentation';
+import { CreateUser, CreateUserCommand } from '../../usecases/create-user';
+import {
+ SwitchEnvironment,
+ SwitchEnvironmentCommand,
+} from '../../usecases/switch-environment';
+import {
+ SwitchOrganization,
+ SwitchOrganizationCommand,
+} from '../../usecases/switch-organization';
+import {
+ buildAuthServiceKey,
buildEnvironmentByApiKey,
buildSubscriberKey,
buildUserKey,
- buildAuthServiceKey,
-} from '@novu/application-generic';
-
-import { CreateUserCommand } from '../../user/usecases/create-user/create-user.dto';
-import { CreateUser } from '../../user/usecases/create-user/create-user.usecase';
-import { SwitchEnvironmentCommand } from '../usecases/switch-environment/switch-environment.command';
-import { SwitchEnvironment } from '../usecases/switch-environment/switch-environment.usecase';
-import { SwitchOrganization } from '../usecases/switch-organization/switch-organization.usecase';
-import { SwitchOrganizationCommand } from '../usecases/switch-organization/switch-organization.command';
-import { normalizeEmail } from '../../shared/helpers/email-normalization.service';
-import { ApiException } from '../../shared/exceptions/api.exception';
+ CachedEntity,
+} from '../cache';
+import { normalizeEmail } from '../../utils/email-normalization';
@Injectable()
export class AuthService {
@@ -45,15 +61,23 @@ export class AuthService {
private organizationRepository: OrganizationRepository,
private environmentRepository: EnvironmentRepository,
private memberRepository: MemberRepository,
- @Inject(forwardRef(() => SwitchOrganization)) private switchOrganizationUsecase: SwitchOrganization,
- @Inject(forwardRef(() => SwitchEnvironment)) private switchEnvironmentUsecase: SwitchEnvironment
+ @Inject(forwardRef(() => SwitchOrganization))
+ private switchOrganizationUsecase: SwitchOrganization,
+ @Inject(forwardRef(() => SwitchEnvironment))
+ private switchEnvironmentUsecase: SwitchEnvironment
) {}
async authenticate(
authProvider: AuthProviderEnum,
accessToken: string,
refreshToken: string,
- profile: { name: string; login: string; email: string; avatar_url: string; id: string },
+ profile: {
+ name: string;
+ login: string;
+ email: string;
+ avatar_url: string;
+ id: string;
+ },
distinctId: string,
origin?: SignUpOriginEnum
) {
@@ -62,12 +86,19 @@ export class AuthService {
let newUser = false;
if (!user) {
+ const firstName = profile.name
+ ? profile.name.split(' ').slice(0, -1).join(' ')
+ : profile.login;
+ const lastName = profile.name
+ ? profile.name.split(' ').slice(-1).join(' ')
+ : null;
+
user = await this.createUserUsecase.execute(
CreateUserCommand.create({
picture: profile.avatar_url,
email,
- lastName: profile.name ? profile.name.split(' ').slice(-1).join(' ') : null,
- firstName: profile.name ? profile.name.split(' ').slice(0, -1).join(' ') : profile.login,
+ firstName,
+ lastName,
auth: {
username: profile.login,
profileId: profile.id,
@@ -88,27 +119,11 @@ export class AuthService {
origin: origin,
});
} else {
- if (authProvider === AuthProviderEnum.GITHUB) {
- const withoutUsername = user.tokens.find(
- (i) => i.provider === AuthProviderEnum.GITHUB && !i.username && String(i.providerId) === String(profile.id)
- );
-
- if (withoutUsername) {
- await this.userRepository.update(
- {
- _id: user._id,
- 'tokens.providerId': profile.id,
- },
- {
- $set: {
- 'tokens.$.username': profile.login,
- },
- }
- );
-
- user = await this.userRepository.findById(user._id);
- if (!user) throw new ApiException('User not found');
- }
+ if (
+ authProvider === AuthProviderEnum.GITHUB ||
+ authProvider === AuthProviderEnum.GOOGLE
+ ) {
+ user = await this.updateUserUsername(user, profile, authProvider);
}
this.analyticsService.track('[Authentication] - Login', user._id, {
@@ -124,6 +139,44 @@ export class AuthService {
};
}
+ private async updateUserUsername(
+ user: UserEntity,
+ profile: {
+ name: string;
+ login: string;
+ email: string;
+ avatar_url: string;
+ id: string;
+ },
+ authProvider: AuthProviderEnum
+ ) {
+ const withoutUsername = user.tokens.find(
+ (token) =>
+ token.provider === authProvider &&
+ !token.username &&
+ String(token.providerId) === String(profile.id)
+ );
+
+ if (withoutUsername) {
+ await this.userRepository.update(
+ {
+ _id: user._id,
+ 'tokens.providerId': profile.id,
+ },
+ {
+ $set: {
+ 'tokens.$.username': profile.login,
+ },
+ }
+ );
+
+ user = await this.userRepository.findById(user._id);
+ if (!user) throw new ApiException('User not found');
+ }
+
+ return user;
+ }
+
async refreshToken(userId: string) {
const user = await this.getUser({ _id: userId });
if (!user) throw new UnauthorizedException('User not found');
@@ -132,13 +185,21 @@ export class AuthService {
}
@Instrument()
- async isAuthenticatedForOrganization(userId: string, organizationId: string): Promise
{
- return !!(await this.memberRepository.isMemberOfOrganization(organizationId, userId));
+ async isAuthenticatedForOrganization(
+ userId: string,
+ organizationId: string
+ ): Promise {
+ return !!(await this.memberRepository.isMemberOfOrganization(
+ organizationId,
+ userId
+ ));
}
@Instrument()
async apiKeyAuthenticate(apiKey: string) {
- const { environment, user, key, error } = await this.getUserData({ apiKey });
+ const { environment, user, key, error } = await this.getUserData({
+ apiKey,
+ });
if (error) throw new UnauthorizedException(error);
if (!environment) throw new UnauthorizedException('API Key not found');
@@ -152,10 +213,17 @@ export class AuthService {
if (!key) throw new UnauthorizedException('API Key not found');
- return await this.getApiSignedToken(user, environment._organizationId, environment._id, key.key);
+ return await this.getApiSignedToken(
+ user,
+ environment._organizationId,
+ environment._id,
+ key.key
+ );
}
- async getSubscriberWidgetToken(subscriber: SubscriberEntity): Promise {
+ async getSubscriberWidgetToken(
+ subscriber: SubscriberEntity
+ ): Promise {
return this.jwtService.sign(
{
_id: subscriber._id,
@@ -201,20 +269,26 @@ export class AuthService {
}
async generateUserToken(user: UserEntity) {
- const userActiveOrganizations = await this.organizationRepository.findUserActiveOrganizations(user._id);
+ const userActiveOrganizations =
+ await this.organizationRepository.findUserActiveOrganizations(user._id);
if (userActiveOrganizations && userActiveOrganizations.length) {
const organizationToSwitch = userActiveOrganizations[0];
- const userActiveProjects = await this.environmentRepository.findOrganizationEnvironments(
- organizationToSwitch._id
- );
+ const userActiveProjects =
+ await this.environmentRepository.findOrganizationEnvironments(
+ organizationToSwitch._id
+ );
let environmentToSwitch = userActiveProjects[0];
- const reduceEnvsToOnlyDevelopment = (prev, current) => (current.name === 'Development' ? current : prev);
+ const reduceEnvsToOnlyDevelopment = (prev, current) =>
+ current.name === 'Development' ? current : prev;
if (userActiveProjects.length > 1) {
- environmentToSwitch = userActiveProjects.reduce(reduceEnvsToOnlyDevelopment, environmentToSwitch);
+ environmentToSwitch = userActiveProjects.reduce(
+ reduceEnvsToOnlyDevelopment,
+ environmentToSwitch
+ );
}
if (environmentToSwitch) {
@@ -278,14 +352,21 @@ export class AuthService {
if (!user) throw new UnauthorizedException('User not found');
if (payload.organizationId && !isMember) {
- throw new UnauthorizedException(`No authorized for organization ${payload.organizationId}`);
+ throw new UnauthorizedException(
+ `No authorized for organization ${payload.organizationId}`
+ );
}
return user;
}
- async validateSubscriber(payload: ISubscriberJwt): Promise {
- return await this.getSubscriber({ _environmentId: payload.environmentId, subscriberId: payload.subscriberId });
+ async validateSubscriber(
+ payload: ISubscriberJwt
+ ): Promise {
+ return await this.getSubscriber({
+ _environmentId: payload.environmentId,
+ subscriberId: payload.subscriberId,
+ });
}
async decodeJwt(token: string) {
@@ -334,8 +415,17 @@ export class AuthService {
subscriberId: command.subscriberId,
}),
})
- private async getSubscriber({ subscriberId, _environmentId }: { subscriberId: string; _environmentId: string }) {
- return await this.subscriberRepository.findBySubscriberId(_environmentId, subscriberId);
+ private async getSubscriber({
+ subscriberId,
+ _environmentId,
+ }: {
+ subscriberId: string;
+ _environmentId: string;
+ }): Promise {
+ return await this.subscriberRepository.findBySubscriberId(
+ _environmentId,
+ subscriberId
+ );
}
@CachedEntity({
@@ -344,11 +434,12 @@ export class AuthService {
apiKey: apiKey,
}),
})
- private async getUserData({
- apiKey,
- }: {
- apiKey: string;
- }): Promise<{ environment?: EnvironmentEntity; user?: UserEntity; key?: IApiKey; error?: string }> {
+ private async getUserData({ apiKey }: { apiKey: string }): Promise<{
+ environment?: EnvironmentEntity;
+ user?: UserEntity;
+ key?: IApiKey;
+ error?: string;
+ }> {
const environment = await this.environmentRepository.findByApiKey(apiKey);
if (!environment) {
return { error: 'API Key not found' };
diff --git a/packages/application-generic/src/services/auth/index.ts b/packages/application-generic/src/services/auth/index.ts
new file mode 100644
index 00000000000..ef64497a51f
--- /dev/null
+++ b/packages/application-generic/src/services/auth/index.ts
@@ -0,0 +1,2 @@
+export * from './auth.service';
+export * from './shared';
diff --git a/packages/application-generic/src/services/auth/shared.ts b/packages/application-generic/src/services/auth/shared.ts
new file mode 100644
index 00000000000..2dc554d42dc
--- /dev/null
+++ b/packages/application-generic/src/services/auth/shared.ts
@@ -0,0 +1,53 @@
+export const buildOauthRedirectUrl = (request): string => {
+ let url = process.env.FRONT_BASE_URL + '/auth/login';
+
+ if (!request.user || !request.user.token) {
+ return `${url}?error=AuthenticationError`;
+ }
+
+ const redirectUrl = JSON.parse(request.query.state).redirectUrl;
+
+ /**
+ * Make sure we only allow localhost redirects for CLI use and our own success route
+ * https://github.com/novuhq/novu/security/code-scanning/3
+ */
+ if (
+ redirectUrl &&
+ redirectUrl.startsWith('http://localhost:') &&
+ !redirectUrl.includes('@')
+ ) {
+ url = redirectUrl;
+ }
+
+ url += `?token=${request.user.token}`;
+
+ if (request.user.newUser) {
+ url += '&newUser=true';
+ }
+
+ /**
+ * partnerCode, next and configurationId are required during external partners integration
+ * such as vercel integration etc
+ */
+ const partnerCode = JSON.parse(request.query.state).partnerCode;
+ if (partnerCode) {
+ url += `&code=${partnerCode}`;
+ }
+
+ const next = JSON.parse(request.query.state).next;
+ if (next) {
+ url += `&next=${next}`;
+ }
+
+ const configurationId = JSON.parse(request.query.state).configurationId;
+ if (configurationId) {
+ url += `&configurationId=${configurationId}`;
+ }
+
+ const invitationToken = JSON.parse(request.query.state).invitationToken;
+ if (invitationToken) {
+ url += `&invitationToken=${invitationToken}`;
+ }
+
+ return url;
+};
diff --git a/packages/application-generic/src/services/in-memory-provider/in-memory-provider.service.ts b/packages/application-generic/src/services/in-memory-provider/in-memory-provider.service.ts
index 33d9a9d66c6..ecc60e20182 100644
--- a/packages/application-generic/src/services/in-memory-provider/in-memory-provider.service.ts
+++ b/packages/application-generic/src/services/in-memory-provider/in-memory-provider.service.ts
@@ -116,6 +116,7 @@ export class InMemoryProviderService {
public isClusterMode(): boolean {
const isClusterModeEnabled = this.getIsInMemoryClusterModeEnabled.execute();
+
Logger.log(
this.descriptiveLogMessage(
`Cluster mode ${
@@ -251,6 +252,13 @@ export class InMemoryProviderService {
const { getClient, getConfig, isClientReady } = getClientAndConfig();
+ console.log(
+ getClient(),
+ getConfig(),
+ isClientReady(this.provider),
+ LOG_CONTEXT
+ );
+
this.isProviderClientReady = isClientReady;
this.inMemoryProviderConfig = getConfig();
const { host, port, ttl } = getConfig();
diff --git a/packages/application-generic/src/services/in-memory-provider/providers/elasticache-cluster-provider.ts b/packages/application-generic/src/services/in-memory-provider/providers/elasticache-cluster-provider.ts
index 17fccb7cbcb..5b99a9055e6 100644
--- a/packages/application-generic/src/services/in-memory-provider/providers/elasticache-cluster-provider.ts
+++ b/packages/application-generic/src/services/in-memory-provider/providers/elasticache-cluster-provider.ts
@@ -52,7 +52,9 @@ export const getElasticacheClusterProviderConfig =
};
const host = redisClusterConfig.host;
- const port = Number(redisClusterConfig.port);
+ const port = redisClusterConfig.port
+ ? Number(redisClusterConfig.port)
+ : undefined;
const password = redisClusterConfig.password;
const connectTimeout = redisClusterConfig.connectTimeout
? Number(redisClusterConfig.connectTimeout)
diff --git a/packages/application-generic/src/services/in-memory-provider/providers/memory-db-cluster-provider.ts b/packages/application-generic/src/services/in-memory-provider/providers/memory-db-cluster-provider.ts
index 5c000896bda..a92a07168c3 100644
--- a/packages/application-generic/src/services/in-memory-provider/providers/memory-db-cluster-provider.ts
+++ b/packages/application-generic/src/services/in-memory-provider/providers/memory-db-cluster-provider.ts
@@ -55,7 +55,9 @@ export const getMemoryDbClusterProviderConfig =
};
const host = redisClusterConfig.host;
- const port = Number(redisClusterConfig.port);
+ const port = redisClusterConfig.port
+ ? Number(redisClusterConfig.port)
+ : undefined;
const username = redisClusterConfig.username;
const password = redisClusterConfig.password;
const connectTimeout = redisClusterConfig.connectTimeout
diff --git a/packages/application-generic/src/services/in-memory-provider/providers/redis-provider.ts b/packages/application-generic/src/services/in-memory-provider/providers/redis-provider.ts
index 239e4af51c2..9fc4bc6d72e 100644
--- a/packages/application-generic/src/services/in-memory-provider/providers/redis-provider.ts
+++ b/packages/application-generic/src/services/in-memory-provider/providers/redis-provider.ts
@@ -53,8 +53,8 @@ export const getRedisProviderConfig = (): IRedisProviderConfig => {
tls: process.env.REDIS_TLS as ConnectionOptions,
};
- const db = Number(redisConfig.db);
- const port = Number(redisConfig.port) || DEFAULT_PORT;
+ const db = redisConfig.db ? Number(redisConfig.db) : undefined;
+ const port = redisConfig.port ? Number(redisConfig.port) : DEFAULT_PORT;
const host = redisConfig.host || DEFAULT_HOST;
const password = redisConfig.password;
const connectTimeout = redisConfig.connectTimeout
diff --git a/packages/application-generic/src/services/index.ts b/packages/application-generic/src/services/index.ts
index ae96354a041..f3f71f1989b 100644
--- a/packages/application-generic/src/services/index.ts
+++ b/packages/application-generic/src/services/index.ts
@@ -23,3 +23,4 @@ export {
WorkerOptions,
OldInstanceBullMqService,
} from './bull-mq';
+export * from './auth';
diff --git a/packages/application-generic/src/services/queues/inbound-parse-queue.service.spec.ts b/packages/application-generic/src/services/queues/inbound-parse-queue.service.spec.ts
new file mode 100644
index 00000000000..5ed92a3231d
--- /dev/null
+++ b/packages/application-generic/src/services/queues/inbound-parse-queue.service.spec.ts
@@ -0,0 +1,147 @@
+import { Test } from '@nestjs/testing';
+
+import { InboundParseQueueService } from './inbound-parse-queue.service';
+
+let inboundParseQueueService: InboundParseQueueService;
+
+describe('Inbound Parse Queue service', () => {
+ describe('General', () => {
+ beforeAll(async () => {
+ inboundParseQueueService = new InboundParseQueueService();
+ await inboundParseQueueService.queue.obliterate();
+ });
+
+ beforeEach(async () => {
+ await inboundParseQueueService.queue.drain();
+ });
+
+ afterAll(async () => {
+ await inboundParseQueueService.gracefulShutdown();
+ });
+
+ it('should be initialised properly', async () => {
+ expect(inboundParseQueueService).toBeDefined();
+ expect(Object.keys(inboundParseQueueService)).toEqual(
+ expect.arrayContaining([
+ 'topic',
+ 'DEFAULT_ATTEMPTS',
+ 'instance',
+ 'queue',
+ ])
+ );
+ expect(inboundParseQueueService.DEFAULT_ATTEMPTS).toEqual(3);
+ expect(inboundParseQueueService.topic).toEqual('inbound-parse-mail');
+ expect(await inboundParseQueueService.bullMqService.getStatus()).toEqual({
+ queueIsPaused: false,
+ queueName: 'inbound-parse-mail',
+ workerName: undefined,
+ workerIsPaused: undefined,
+ workerIsRunning: undefined,
+ });
+ expect(await inboundParseQueueService.isPaused()).toEqual(false);
+ expect(inboundParseQueueService.queue).toMatchObject(
+ expect.objectContaining({
+ _events: {},
+ _eventsCount: 0,
+ _maxListeners: undefined,
+ name: 'inbound-parse-mail',
+ jobsOpts: {
+ removeOnComplete: true,
+ },
+ })
+ );
+ expect(inboundParseQueueService.queue.opts.prefix).toEqual('bull');
+ });
+
+ it('should add a job in the queue', async () => {
+ const jobId = 'inbound-parse-mail-job-id';
+ const _environmentId = 'inbound-parse-mail-environment-id';
+ const _organizationId = 'inbound-parse-mail-organization-id';
+ const _userId = 'inbound-parse-mail-user-id';
+ const jobData = {
+ _id: jobId,
+ test: 'inbound-parse-mail-job-data',
+ _environmentId,
+ _organizationId,
+ _userId,
+ };
+ await inboundParseQueueService.add(jobId, jobData, _organizationId);
+
+ expect(await inboundParseQueueService.queue.getActiveCount()).toEqual(0);
+ expect(await inboundParseQueueService.queue.getWaitingCount()).toEqual(1);
+
+ const inboundParseQueueJobs =
+ await inboundParseQueueService.queue.getJobs();
+ expect(inboundParseQueueJobs.length).toEqual(1);
+ const [inboundParseQueueJob] = inboundParseQueueJobs;
+ expect(inboundParseQueueJob).toMatchObject(
+ expect.objectContaining({
+ id: '1',
+ name: jobId,
+ data: jobData,
+ attemptsMade: 0,
+ })
+ );
+ });
+
+ it('should add a minimal job in the queue', async () => {
+ const jobId = 'inbound-parse-mail-job-id-2';
+ const _environmentId = 'inbound-parse-mail-environment-id';
+ const _organizationId = 'inbound-parse-mail-organization-id';
+ const _userId = 'inbound-parse-mail-user-id';
+ const jobData = {
+ _id: jobId,
+ test: 'inbound-parse-mail-job-data-2',
+ _environmentId,
+ _organizationId,
+ _userId,
+ };
+ await inboundParseQueueService.addMinimalJob(
+ jobId,
+ jobData,
+ _organizationId
+ );
+
+ expect(await inboundParseQueueService.queue.getActiveCount()).toEqual(0);
+ expect(await inboundParseQueueService.queue.getWaitingCount()).toEqual(1);
+
+ const inboundParseQueueJobs =
+ await inboundParseQueueService.queue.getJobs();
+ expect(inboundParseQueueJobs.length).toEqual(1);
+ const [inboundParseQueueJob] = inboundParseQueueJobs;
+ expect(inboundParseQueueJob).toMatchObject(
+ expect.objectContaining({
+ id: '2',
+ name: jobId,
+ data: {
+ _id: jobId,
+ _environmentId,
+ _organizationId,
+ _userId,
+ },
+ attemptsMade: 0,
+ })
+ );
+ });
+ });
+
+ describe('Cluster mode', () => {
+ beforeAll(async () => {
+ process.env.IS_IN_MEMORY_CLUSTER_MODE_ENABLED = 'true';
+
+ inboundParseQueueService = new InboundParseQueueService();
+ await inboundParseQueueService.queue.obliterate();
+ });
+
+ afterAll(async () => {
+ await inboundParseQueueService.gracefulShutdown();
+ process.env.IS_IN_MEMORY_CLUSTER_MODE_ENABLED = 'false';
+ });
+
+ it('should have prefix in cluster mode', async () => {
+ expect(inboundParseQueueService.queue.opts.prefix).toEqual(
+ '{inbound-parse-mail}'
+ );
+ });
+ });
+});
diff --git a/packages/application-generic/src/services/queues/inbound-parse-queue.service.ts b/packages/application-generic/src/services/queues/inbound-parse-queue.service.ts
index 3e1a5f4096e..6dc20a8ec4d 100644
--- a/packages/application-generic/src/services/queues/inbound-parse-queue.service.ts
+++ b/packages/application-generic/src/services/queues/inbound-parse-queue.service.ts
@@ -3,6 +3,8 @@ import { JobTopicNameEnum } from '@novu/shared';
import { QueueBaseService } from './index';
+import { QueueOptions } from '../bull-mq';
+
const LOG_CONTEXT = 'InboundParseQueueService';
@Injectable()
@@ -12,6 +14,20 @@ export class InboundParseQueueService extends QueueBaseService {
Logger.log(`Creating queue ${this.topic}`, LOG_CONTEXT);
- this.createQueue();
+ this.createQueue(this.getOverrideOptions());
+ }
+
+ private getOverrideOptions(): QueueOptions {
+ return {
+ defaultJobOptions: {
+ attempts: 5,
+ backoff: {
+ delay: 4000,
+ type: 'exponential',
+ },
+ removeOnComplete: true,
+ removeOnFail: true,
+ },
+ };
}
}
diff --git a/packages/application-generic/src/services/queues/queue-base.service.ts b/packages/application-generic/src/services/queues/queue-base.service.ts
index 55697a58f4e..02dc5359990 100644
--- a/packages/application-generic/src/services/queues/queue-base.service.ts
+++ b/packages/application-generic/src/services/queues/queue-base.service.ts
@@ -19,8 +19,18 @@ export class QueueBaseService {
return this.instance;
}
- public createQueue(): void {
- this.queue = this.instance.createQueue(this.topic, this.getQueueOptions());
+ public createQueue(overrideOptions?: QueueOptions): void {
+ const options = {
+ ...this.getQueueOptions(),
+ ...(overrideOptions && {
+ defaultJobOptions: {
+ ...this.getQueueOptions().defaultJobOptions,
+ ...overrideOptions.defaultJobOptions,
+ },
+ }),
+ };
+
+ this.queue = this.instance.createQueue(this.topic, options);
}
private getQueueOptions(): QueueOptions {
diff --git a/apps/api/src/app/user/usecases/create-user/create-user.dto.ts b/packages/application-generic/src/usecases/create-user/create-user.command.ts
similarity index 83%
rename from apps/api/src/app/user/usecases/create-user/create-user.dto.ts
rename to packages/application-generic/src/usecases/create-user/create-user.command.ts
index 8a5771e4464..334161fdc27 100644
--- a/apps/api/src/app/user/usecases/create-user/create-user.dto.ts
+++ b/packages/application-generic/src/usecases/create-user/create-user.command.ts
@@ -1,5 +1,5 @@
import { AuthProviderEnum } from '@novu/shared';
-import { BaseCommand } from '../../../shared/commands/base.command';
+import { BaseCommand } from '../../commands';
export class CreateUserCommand extends BaseCommand {
email: string;
diff --git a/apps/api/src/app/user/usecases/create-user/create-user.usecase.ts b/packages/application-generic/src/usecases/create-user/create-user.usecase.ts
similarity index 94%
rename from apps/api/src/app/user/usecases/create-user/create-user.usecase.ts
rename to packages/application-generic/src/usecases/create-user/create-user.usecase.ts
index bdfbe87a2c3..a365ae2a580 100644
--- a/apps/api/src/app/user/usecases/create-user/create-user.usecase.ts
+++ b/packages/application-generic/src/usecases/create-user/create-user.usecase.ts
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { UserEntity, UserRepository } from '@novu/dal';
-import { CreateUserCommand } from './create-user.dto';
+import { CreateUserCommand } from './create-user.command';
@Injectable()
export class CreateUser {
diff --git a/packages/application-generic/src/usecases/create-user/index.ts b/packages/application-generic/src/usecases/create-user/index.ts
new file mode 100644
index 00000000000..83b360e0164
--- /dev/null
+++ b/packages/application-generic/src/usecases/create-user/index.ts
@@ -0,0 +1,2 @@
+export * from './create-user.usecase';
+export * from './create-user.command';
diff --git a/packages/application-generic/src/usecases/index.ts b/packages/application-generic/src/usecases/index.ts
index 76aaf8cee1e..cec41c573b7 100644
--- a/packages/application-generic/src/usecases/index.ts
+++ b/packages/application-generic/src/usecases/index.ts
@@ -25,3 +25,6 @@ export * from './create-tenant';
export * from './get-tenant';
export * from './process-tenant';
export * from './conditions-filter';
+export * from './switch-environment';
+export * from './switch-organization';
+export * from './create-user';
diff --git a/packages/application-generic/src/usecases/switch-environment/index.ts b/packages/application-generic/src/usecases/switch-environment/index.ts
new file mode 100644
index 00000000000..cb3beb29002
--- /dev/null
+++ b/packages/application-generic/src/usecases/switch-environment/index.ts
@@ -0,0 +1,2 @@
+export * from './switch-environment.command';
+export * from './switch-environment.usecase';
diff --git a/apps/api/src/app/auth/usecases/switch-environment/switch-environment.command.ts b/packages/application-generic/src/usecases/switch-environment/switch-environment.command.ts
similarity index 65%
rename from apps/api/src/app/auth/usecases/switch-environment/switch-environment.command.ts
rename to packages/application-generic/src/usecases/switch-environment/switch-environment.command.ts
index 5bf2c7cb684..de73e16cffe 100644
--- a/apps/api/src/app/auth/usecases/switch-environment/switch-environment.command.ts
+++ b/packages/application-generic/src/usecases/switch-environment/switch-environment.command.ts
@@ -1,5 +1,5 @@
import { IsNotEmpty } from 'class-validator';
-import { OrganizationCommand } from '../../../shared/commands/organization.command';
+import { OrganizationCommand } from '../../commands';
export class SwitchEnvironmentCommand extends OrganizationCommand {
@IsNotEmpty()
diff --git a/apps/api/src/app/auth/usecases/switch-environment/switch-environment.usecase.ts b/packages/application-generic/src/usecases/switch-environment/switch-environment.usecase.ts
similarity index 61%
rename from apps/api/src/app/auth/usecases/switch-environment/switch-environment.usecase.ts
rename to packages/application-generic/src/usecases/switch-environment/switch-environment.usecase.ts
index 8963c0793f5..295769ea924 100644
--- a/apps/api/src/app/auth/usecases/switch-environment/switch-environment.usecase.ts
+++ b/packages/application-generic/src/usecases/switch-environment/switch-environment.usecase.ts
@@ -1,7 +1,17 @@
-import { forwardRef, Inject, Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common';
-import { EnvironmentRepository, MemberRepository, OrganizationRepository, UserRepository } from '@novu/dal';
-import { AuthService } from '../../services/auth.service';
+import {
+ forwardRef,
+ Inject,
+ Injectable,
+ NotFoundException,
+ UnauthorizedException,
+} from '@nestjs/common';
+import {
+ EnvironmentRepository,
+ MemberRepository,
+ UserRepository,
+} from '@novu/dal';
import { SwitchEnvironmentCommand } from './switch-environment.command';
+import { AuthService } from '../../services/auth/auth.service';
@Injectable()
export class SwitchEnvironment {
@@ -13,19 +23,29 @@ export class SwitchEnvironment {
) {}
async execute(command: SwitchEnvironmentCommand) {
- const project = await this.environmentRepository.findById(command.newEnvironmentId);
+ const project = await this.environmentRepository.findById(
+ command.newEnvironmentId
+ );
if (!project) throw new NotFoundException('Environment not found');
if (project._organizationId !== command.organizationId) {
throw new UnauthorizedException('Not authorized for organization');
}
- const member = await this.memberRepository.findMemberByUserId(command.organizationId, command.userId);
+ const member = await this.memberRepository.findMemberByUserId(
+ command.organizationId,
+ command.userId
+ );
if (!member) throw new NotFoundException('Member is not found');
const user = await this.userRepository.findById(command.userId);
if (!user) throw new NotFoundException('User is not found');
- const token = await this.authService.getSignedToken(user, command.organizationId, member, command.newEnvironmentId);
+ const token = await this.authService.getSignedToken(
+ user,
+ command.organizationId,
+ member,
+ command.newEnvironmentId
+ );
return token;
}
diff --git a/packages/application-generic/src/usecases/switch-organization/index.ts b/packages/application-generic/src/usecases/switch-organization/index.ts
new file mode 100644
index 00000000000..3d2cfef5222
--- /dev/null
+++ b/packages/application-generic/src/usecases/switch-organization/index.ts
@@ -0,0 +1,2 @@
+export * from './switch-organization.command';
+export * from './switch-organization.usecase';
diff --git a/apps/api/src/app/auth/usecases/switch-organization/switch-organization.command.ts b/packages/application-generic/src/usecases/switch-organization/switch-organization.command.ts
similarity index 65%
rename from apps/api/src/app/auth/usecases/switch-organization/switch-organization.command.ts
rename to packages/application-generic/src/usecases/switch-organization/switch-organization.command.ts
index 35b5ad42fdf..390622e513f 100644
--- a/apps/api/src/app/auth/usecases/switch-organization/switch-organization.command.ts
+++ b/packages/application-generic/src/usecases/switch-organization/switch-organization.command.ts
@@ -1,5 +1,5 @@
import { IsNotEmpty } from 'class-validator';
-import { AuthenticatedCommand } from '../../../shared/commands/authenticated.command';
+import { AuthenticatedCommand } from '../../commands';
export class SwitchOrganizationCommand extends AuthenticatedCommand {
@IsNotEmpty()
diff --git a/apps/api/src/app/auth/usecases/switch-organization/switch-organization.usecase.ts b/packages/application-generic/src/usecases/switch-organization/switch-organization.usecase.ts
similarity index 55%
rename from apps/api/src/app/auth/usecases/switch-organization/switch-organization.usecase.ts
rename to packages/application-generic/src/usecases/switch-organization/switch-organization.usecase.ts
index 527d20b0211..e6108ffbc75 100644
--- a/apps/api/src/app/auth/usecases/switch-organization/switch-organization.usecase.ts
+++ b/packages/application-generic/src/usecases/switch-organization/switch-organization.usecase.ts
@@ -1,8 +1,18 @@
-import { forwardRef, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
-import { MemberRepository, OrganizationRepository, UserRepository, EnvironmentRepository } from '@novu/dal';
+import {
+ forwardRef,
+ Inject,
+ Injectable,
+ UnauthorizedException,
+} from '@nestjs/common';
+import {
+ MemberRepository,
+ OrganizationRepository,
+ UserRepository,
+ EnvironmentRepository,
+} from '@novu/dal';
import { SwitchOrganizationCommand } from './switch-organization.command';
-import { AuthService } from '../../services/auth.service';
-import { ApiException } from '../../../shared/exceptions/api.exception';
+import { AuthService } from '../../services/auth/auth.service';
+import { ApiException } from '../../utils/exceptions';
@Injectable()
export class SwitchOrganization {
@@ -15,15 +25,21 @@ export class SwitchOrganization {
) {}
async execute(command: SwitchOrganizationCommand): Promise {
- const isAuthenticated = await this.authService.isAuthenticatedForOrganization(
- command.userId,
- command.newOrganizationId
- );
+ const isAuthenticated =
+ await this.authService.isAuthenticatedForOrganization(
+ command.userId,
+ command.newOrganizationId
+ );
if (!isAuthenticated) {
- throw new UnauthorizedException(`Not authorized for organization ${command.newOrganizationId}`);
+ throw new UnauthorizedException(
+ `Not authorized for organization ${command.newOrganizationId}`
+ );
}
- const member = await this.memberRepository.findMemberByUserId(command.newOrganizationId, command.userId);
+ const member = await this.memberRepository.findMemberByUserId(
+ command.newOrganizationId,
+ command.userId
+ );
if (!member) throw new ApiException('Member not found');
const user = await this.userRepository.findById(command.userId);
@@ -34,7 +50,12 @@ export class SwitchOrganization {
_parentId: { $exists: false },
});
- const token = await this.authService.getSignedToken(user, command.newOrganizationId, member, environment?._id);
+ const token = await this.authService.getSignedToken(
+ user,
+ command.newOrganizationId,
+ member,
+ environment?._id
+ );
return token;
}
diff --git a/packages/application-generic/src/utils/email-normalization.ts b/packages/application-generic/src/utils/email-normalization.ts
new file mode 100644
index 00000000000..fa18f9f735e
--- /dev/null
+++ b/packages/application-generic/src/utils/email-normalization.ts
@@ -0,0 +1,48 @@
+const PLUS_ONLY = /\+.*$/;
+const PLUS_AND_DOT = /\.|\+.*$/g;
+const normalizableProviders = {
+ 'gmail.com': {
+ cut: PLUS_AND_DOT,
+ },
+ 'googlemail.com': {
+ cut: PLUS_AND_DOT,
+ aliasOf: 'gmail.com',
+ },
+ 'hotmail.com': {
+ cut: PLUS_ONLY,
+ },
+ 'live.com': {
+ cut: PLUS_AND_DOT,
+ },
+ 'outlook.com': {
+ cut: PLUS_ONLY,
+ },
+};
+
+export function normalizeEmail(email: string): string {
+ if (typeof email !== 'string') {
+ throw new TypeError('normalize-email expects a string');
+ }
+
+ const lowerCasedEmail = email.toLowerCase();
+ const emailParts = lowerCasedEmail.split(/@/);
+
+ if (emailParts.length !== 2) {
+ return email;
+ }
+
+ let username = emailParts[0];
+ let domain = emailParts[1];
+
+ if (normalizableProviders.hasOwnProperty(domain)) {
+ if (normalizableProviders[domain].hasOwnProperty('cut')) {
+ username = username.replace(normalizableProviders[domain].cut, '');
+ }
+
+ if (normalizableProviders[domain].hasOwnProperty('aliasOf')) {
+ domain = normalizableProviders[domain].aliasOf;
+ }
+ }
+
+ return `${username}@${domain}`;
+}
diff --git a/packages/node/README.md b/packages/node/README.md
index 7f117943f31..12a5a869892 100644
--- a/packages/node/README.md
+++ b/packages/node/README.md
@@ -29,17 +29,18 @@ The ultimate service for managing multi-channel notifications with a single API.
·
Twitter
·
- Notifications Directory
- .
+ Notifications Directory.
Read our blog
+
## ⭐️ Why
-Building a notification system is hard, at first it seems like just sending an email but in reality it's just the beginning. In today's world users expect multichannel communication experience over email, sms, push, chat and more... An ever-growing list of providers are popping up each day, and notifications are spread around the code. Novu's goal is to simplify notifications and provide developers the tools to create meaningful communication between the system and its users.
+
+Building a notification system is hard, at first it seems like just sending an email but in reality, it's just the beginning. In today's world users expect multi-channel communication experience over email, sms, push, chat, and more... An ever-growing list of providers is popping up each day, and notifications are spread around the code. Novu's goal is to simplify notifications and provide developers the tools to create meaningful communication between the system and its users.
## ✨ Features
- 🌈 Single API for all messaging providers (Email, SMS, Push, Chat)
-- 💅 Easily manage notification over multiple channels
+- 💅 Easily manage notifications over multiple channels
- 🚀 Equipped with a templating engine for advanced layouts and designs
- 🛡 Built-in protection for missing variables
- 📦 Easy to set up and integrate
@@ -135,3 +136,951 @@ Novu provides a single API to manage providers across multiple channels with a s
## 🔗 Links
- [Home page](https://novu.co/)
+
+
+## SDK Methods
+
+- [Subscribers](#subscribers)
+- [Events](#events)
+- [Workflows](#workflows)
+- [Notification Groups](#notification-groups)
+- [Topics](#topics)
+- [Feeds](#feeds)
+- [Tenants](#tenants)
+- [Messages](#messages)
+- [Changes](#changes)
+- [Environments](#environments)
+- [Layouts](#layouts)
+- [Integrations](#integrations)
+
+
+### Subscribers
+
+- #### List all subscribers
+
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+const page = 0;
+const limit = 20;
+
+await novu.subscribers.list(page,limit)
+```
+
+- #### Identify (create) a new subscriber
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+await novu.subscribers.identify("subscriberId",{
+ firstName: "Pawan";
+ lastName: "Jain";
+ email: "pawan.jain@domain.com";
+ phone: "+1234567890";
+ avatar: "https://gravatar.com/avatar/553b157d82ac2880237566d5a644e5fe?s=400&d=robohash&r=x";
+ locale: "en-US";
+ data: {
+ isDeveloper : true
+ customKey: "customValue"
+ };
+})
+```
+
+
+- #### Bulk create subscribers
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+await novu.subscribers.identify([
+ {
+ subscriberId: "1"
+ firstName: "Pawan";
+ lastName: "Jain";
+ email: "pawan.jain@domain.com";
+ phone: "+1234567890";
+ avatar: "https://gravatar.com/avatar/553b157d82ac2880237566d5a644e5fe?s=400&d=robohash&r=x";
+ locale: "en-US";
+ data: {
+ isDeveloper : true
+ customKey: "customValue"
+ };
+ },
+ {
+ subscriberId: "2"
+ firstName: "John";
+ lastName: "Doe";
+ email: "john.doe@domain.com";
+ phone: "+1234567891";
+ avatar: "https://gravatar.com/avatar/553b157d82ac2880237566d5a644e5fe?s=400&d=robohash&r=x";
+ locale: "en-UK";
+ data: {
+ isDeveloper : false
+ customKey1: "customValue1"
+ };
+ }
+ // more subscribers ...
+])
+```
+
+
+- #### Get a single subscriber
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+await novu.subscribers.get("subscriberId")
+```
+
+- #### Update a subscriber
+
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+await novu.subscribers.update("subscriberId",{
+ firstName: "Pawan";
+ lastName: "Jain";
+ email: "pawan.jain@domain.com";
+ phone: "+1234567890";
+ avatar: "https://gravatar.com/avatar/553b157d82ac2880237566d5a644e5fe?s=400&d=robohash&r=x";
+ locale: "en-US";
+ data: {
+ isDeveloper : true
+ customKey: "customValue"
+ customKey2: "customValue2"
+ };
+})
+```
+
+- #### Update provider credentials
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+// update fcm token
+await novu.subscribers.setCredentials("subscriberId", "fcm", {
+ deviceTokens: ["token1", "token2"]
+})
+
+// update slack webhookurl
+await novu.subscribers.setCredentials("subscriberId", "slack", {
+ webhookUrl: ["webhookUrl"]
+})
+```
+
+- #### Delete provider credentials
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+// delete fcm token
+await novu.subscribers.deleteCredentials("subscriberId", "fcm")
+
+// delete slack webhookurl
+await novu.subscribers.deleteCredentials("subscriberId", "slack")
+```
+
+- #### Delete a subscriber
+
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+await novu.subscribers.delete("subscriberId")
+```
+
+- #### Update online status
+
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+// mark subscriber as offline
+await novu.subscribers.updateOnlineStatus("subscriberId", false)
+```
+
+- #### Get subscriber preference for all workflows
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+await novu.subscribers.getPreference("subscriberId")
+```
+
+- #### Update subscriber preference for a workflow
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+// enable in-app channel
+await novu.subscribers.updatePreference("subscriberId", "workflowId", {
+ channel: {
+ type: "in_app"
+ enabled: true
+ }
+})
+
+
+// disable email channel
+await novu.subscribers.updatePreference("subscriberId", "workflowId", {
+ channel: {
+ type: "email"
+ enabled:
+ }
+})
+```
+
+- #### Get in-app messages (notifications) feed for a subscriber
+
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+const params = {
+ page: 0;
+ limit: 20;
+ // copy this value from in-app editor
+ feedIdentifier: "feedId";
+ seen: true
+ read: false
+ payload: {
+ "customkey": "customValue"
+ }
+}
+
+await novu.subscribers.getNotificationsFeed("subscriberId", params);
+```
+
+- #### Get seen/unseen in-app messages (notifications) count
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+// get seen count
+await novu.subscribers.getUnseenCount("subscriberId", true);
+
+// get unseen count
+await novu.subscribers.getUnseenCount("subscriberId", false);
+```
+
+- #### Mark an in-app message (notification) as seen/unseen/read/unread
+
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+// mark unseen
+await novu.subscribers.markMessageAs("subscriberId", "messageId", {
+ seen: false
+});
+
+// mark seen and unread
+await novu.subscribers.markMessageAs("subscriberId", "messageId", {
+ seen: true,
+ read: false
+});
+```
+
+- #### Mark all in-app messages (notifications) as seen/unseen/read/unread
+
+```ts
+import { Novu, MarkMessagesAsEnum } from '@novu/node';
+
+const novu = new Novu('');
+
+// mark all messages as seen
+await novu.subscribers.markAllMessagesAs("subscriberId", MarkMessageAsEnum.SEEN, "feedId");
+
+// mark all messages as read
+await novu.subscribers.markAllMessagesAs("subscriberId", MarkMessageAsEnum.READ, "feedId");
+```
+
+- #### Mark in-app message (notification) action as seen
+
+```ts
+import { Novu, ButtonTypeEnum, MessageActionStatusEnum } from '@novu/node';
+
+const novu = new Novu('');
+
+// mark a message's primary action button as pending
+await novu.subscribers.markMessageActionSeen("subscriberId", "messageId", ButtonTypeEnum.PRIMARY, {
+ status: MessageActionStatusEnum.PENDING
+});
+
+// mark a message's secondary action button as done
+await novu.subscribers.markMessageActionSeen("subscriberId", "messageId", ButtonTypeEnum.SECONDARY, {
+ status: MessageActionStatusEnum.DONE
+});
+```
+
+### Events
+
+- #### Trigger workflow to one subscriber
+
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+// trigger to existing subscribers
+await novu.subscribers.trigger("workflowIdentifier", {
+ to: "subscriberId",
+ payload: {
+ customKey: "customValue",
+ customKey1: {
+ nestedkey1: "nestedValue1"
+ }
+ },
+ overrides: {
+ email: {
+ from: "support@novu.co"
+ }
+ },
+ // actorId is subscriberId of actor
+ actor: "actorId"
+ tenant: "tenantIdentifier"
+});
+
+// create new subscriber inline with trigger
+await novu.subscribers.trigger("workflowIdentifier", {
+ to: {
+ subscriberId: "1"
+ firstName: "Pawan";
+ lastName: "Jain";
+ email: "pawan.jain@domain.com";
+ phone: "+1234567890";
+ avatar: "https://gravatar.com/avatar/553b157d82ac2880237566d5a644e5fe?s=400&d=robohash&r=x";
+ locale: "en-US";
+ data: {
+ isDeveloper : true
+ customKey: "customValue"
+ };
+ },
+ payload: {},
+ overrides:{} ,
+ actor: "actorId"
+ tenant: "tenantIdentifier"
+});
+```
+
+- #### Trigger workflow to multiple subscribers
+
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+await novu.subscribers.trigger("workflowIdentifier", {
+ to: [ "subscriberId1" , "subscriberId2" ],
+ payload: {},
+ overrides:{} ,
+ actor: "actorId"
+ tenant: "tenantIdentifier"
+});
+
+
+// create new subscribers inline with trigger
+await novu.subscribers.trigger("workflowIdentifier", {
+ to: [
+ {
+ subscriberId: "1"
+ firstName: "Pawan";
+ lastName: "Jain";
+ email: "pawan.jain@domain.com";
+ phone: "+1234567890";
+ avatar: "https://gravatar.com/avatar/553b157d82ac2880237566d5a644e5fe?s=400&d=robohash&r=x";
+ locale: "en-US";
+ data: {
+ isDeveloper : true
+ customKey: "customValue"
+ };
+ },
+ {
+ subscriberId: "2"
+ firstName: "John";
+ lastName: "Doe";
+ email: "john.doe@domain.com";
+ phone: "+1234567891";
+ avatar: "https://gravatar.com/avatar/553b157d82ac2880237566d5a644e5fe?s=400&d=robohash&r=x";
+ locale: "en-UK";
+ data: {
+ isDeveloper : false
+ customKey1: "customValue1"
+ };
+ }
+ ],
+ payload: {},
+ overrides:{} ,
+ actor: "actorId"
+ tenant: "tenantIdentifier"
+});
+```
+
+- #### Trigger to a topic
+```ts
+import { Novu, TriggerRecipientsTypeEnum } from '@novu/node';
+
+const novu = new Novu('');
+
+await novu.events.trigger("workflowIdentifier", {
+ to: {
+ type: TriggerRecipientsTypeEnum.TOPIC;
+ topicKey: TopicKey;
+ }
+})
+```
+
+- #### Bulk trigger multiple workflows to multiple subscribers
+
+There is a limit of 100 items in the array of bulkTrigger.
+
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+await novu.subscribers.bulkTrigger([
+ {
+ name: "workflowIdentifier_1",
+ to: "subscriberId_1",
+ payload: {
+ customKey: "customValue",
+ customKey1: {
+ nestedkey1: "nestedValue1"
+ }
+ },
+ overrides: {
+ email: {
+ from: "support@novu.co"
+ }
+ },
+ // actorId is subscriberId of actor
+ actor: "actorId"
+ tenant: "tenantIdentifier"
+ },
+ {
+ name: "workflowIdentifier_2",
+ to: "subscriberId_2",
+ payload: {
+ customKey: "customValue",
+ customKey1: {
+ nestedkey1: "nestedValue1"
+ }
+ },
+ overrides: {
+ email: {
+ from: "support@novu.co"
+ }
+ },
+ // actorId is subscriberId of actor
+ actor: "actorId"
+ tenant: "tenantIdentifier"
+ }
+])
+```
+
+- #### Broadcast to all subscribers
+
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+await novu.subscribers.broadcast("workflowIdentifier", {
+ payload: {
+ customKey: "customValue",
+ customKey1: {
+ nestedkey1: "nestedValue1"
+ }
+ },
+ overrides: {
+ email: {
+ from: "support@novu.co"
+ }
+ },
+ tenant: "tenantIdentifier"
+})
+```
+
+- #### Cancel the triggered workflow
+
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+await novu.subscribers.cancel("transactionId");
+```
+### Messages
+
+- #### List all messages
+
+```ts
+import { Novu, ChannelTypeEnum } from '@novu/node';
+
+const novu = new Novu('');
+
+const params = {
+ page: 0, // optional
+ limit: 20, // optional
+ subscriberId: "subscriberId" //optional
+ channel: ChannelTypeEnum.EMAIL //optional
+ transactionIds : ["txnId1","txnId2"] //optional
+}
+
+await novu.messages.list(params)
+```
+
+- #### Delete a message by `messageId`
+
+```ts
+import { Novu, ChannelTypeEnum } from '@novu/node';
+
+const novu = new Novu('');
+
+await novu.messages.deleteById("messageId");
+```
+
+### Layouts
+
+- #### Create a layout
+
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+const payload = {
+ content: "Layout Start
{{{body}}}Layout End
",
+ description: "Organisation's first layout",
+ name: "First Layout",
+ identifier: "firstlayout",
+ variables: [
+ {
+ type: "String",
+ name: "body"
+ required: true
+ defValue: ""
+ }
+ ]
+ isDefault: "false"
+}
+await novu.layouts.create(payload);
+```
+
+- #### Update a layout
+
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+const payloadToUpdate = {
+ content: "Layout Start
{{{body}}}Layout End
",
+ description: "Organisation's first layout",
+ name: "First Layout",
+ identifier: "firstlayout",
+ variables: [
+ {
+ type: "String",
+ name: "body"
+ required: true
+ defValue: ""
+ }
+ ]
+ isDefault: false
+}
+await novu.layouts.update("layoutId", payloadToUpdate);
+```
+
+- #### Set a layout as the default layout
+
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+await novu.layouts.setDefault("layoutId");
+```
+
+- #### Get a layout by `layoutId`
+
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+await novu.layouts.get("layoutId");
+```
+
+- #### Delete a layout by `layoutId`
+
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+await novu.layouts.delete("layoutId");
+```
+
+- #### List all layouts
+
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+const params = {
+ page: 0, // optional
+ pageSize: 20, // optional
+ sortBy: "_id"
+ orderBy: -1 //optional
+}
+
+await novu.layouts.list(params);
+```
+
+### Notification Groups
+
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+// create a new notification group
+await novu.notificationGroups.create("Product Updates")
+
+// update an exisiting notification group
+await novu.notificationGroups.update("notificationGroupId", { name: "Changelog Updates"})
+
+// list all notification groups
+await novu.notificationGroups.get()
+
+// get one exisiting notification group
+await novu.notificationGroups.getOne("notificationGroupId")
+
+// delete an existing notification group
+await novu.notificationGroups.delete("notificationGroupId")
+```
+
+### Topics
+
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+const payloadToCreate = {
+ key: "first-topic",
+ name: "First Topic"
+}
+
+// create new topic
+await novu.topics.create(payloadToCreate)
+
+// add subscribers
+await novu.topics.addSubscribers("topicKey", { subscribers: ["subscriberId1", "subscriberId2"] })
+
+// check if subscriber is present in topic
+await novu.topics.checkSubscriber("topicKey", "subscriberId")
+
+// remove subscribers from topic
+await novu.topics.removeSubscribers("topicKey", { subscribers: ["subscriberId1", "subscriberId2"] } )
+
+const topicsListParams = {
+ page: 0, //optional
+ pageSize: 20,
+ key: "topicKey"
+}
+
+// list all topics
+await novu.topics.list(topicsListParams)
+
+// get a topic
+await novu.topics.get("topicKey")
+
+// delete a topic
+await novu.topics.delete("topicKey")
+
+// get a topic
+await novu.topics.rename("topicKey", "New Topic Name")
+```
+
+### Integrations
+
+```ts
+import { Novu, ChannelTypeEnum, ProvidersIdEnum } from '@novu/node';
+
+const novu = new Novu('');
+
+const updatePayload = {
+ name: "SendGrid",
+ identifier: "sendgrid-identifier",
+ credentials: {
+ apiKey: "SUPER_SECRET_API_KEY",
+ from: "sales@novu.co",
+ senderName: "Novu Sales Team"
+ // ... other credentials as per provider
+ },
+ active: true,
+ check: false
+}
+
+const createPayload: {
+ ...updatePayload,
+ channel: ChannelTypeEnum.EMAIL,
+}
+
+// create a new integration
+await novu.integrations.create(ProvidersIdEnum.SendGrid, createPayload)
+
+// update integration
+await novu.integrations.update("integrationId", updatePayload)
+
+// get all integrations
+await novu.integrations.getAll()
+
+// get only active integrations
+await novu.integrations.getActive()
+
+// get webhook provider status
+await novu.integrations.getWebhookProviderStatus(ProvidersIdEnum.SendGrid)
+
+// delete existing integration
+await novu.integrations.delete("integrationId")
+
+// get novu in-app status
+await novu.integrations.getInAppStatus()
+```
+
+### Feeds
+
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+// create new in-app feed
+await novu.feeds.create("Product Updates")
+
+/**
+ * get all in-app feeds
+ * feeds methods returns only feed information
+ * use subscriber.notificationsFeed() for in-app messages
+ */
+await novu.feeds.get()
+
+// delete a feed
+await novu.feeds.delete("feedId")
+```
+
+### Changes
+
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+// get all changes
+await novu.changes.get()
+
+// get changes count
+await novu.changes.getCount()
+
+// apply only one change
+await novu.changes.applyOne("changeId")
+
+// apply many changes
+await novu.changes.applyMany(["changeId1", "changeId2"])
+```
+
+### Environments
+
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+// get current environmet
+await novu.environments.getCurrent()
+
+// create new environment
+await novu.environments.create({
+ name: "Stagging"
+ parentId: "parentEnvironmentId"
+})
+
+// get all environmemts
+await novu.environments.getAll()
+
+// update one environment
+await novu.environments.updateOne("environmentId", {
+ name: "Stagging" // optional
+ parentId: "parentEnvironmentId", // optional
+ identifier: "environmentIdentifier" // optional
+})
+
+// get api keys of environmet
+await novu.environments.getApiKeys()
+
+// regenrate api keys
+await novu.environments.regenerateApiKeys()
+```
+
+### Tenants
+
+```ts
+import { Novu } from '@novu/node';
+
+const novu = new Novu('');
+
+// create new tenat
+await novu.tenants.create("tenantIdentifier", {
+ name: "First Tenant",
+ // optional
+ data: {
+ country: "US",
+ tokens: ["token1", "token2"],
+ isDeveloperTenant : true,
+ numberOfMembers: 2,
+ isSales : undefined
+ }
+})
+
+// update existing tenant
+await novu.tenants.update("tenantIdentifier", {
+ identifier: "tenantIdentifier1",
+ name: "Second Tenant",
+ // optional
+ data: {
+ country: "India",
+ tokens: ["token1", "token2"],
+ isDeveloperTenant : true,
+ numberOfMembers: 2,
+ isSales : undefined
+ }
+})
+
+// list all tenants
+await novu.tenants.list({
+ page: 0, // optional
+ limit: 20 // optional
+})
+
+// delete a tenant
+await novu.tenants.delete("tenantIdentifier")
+
+// get one tenant
+await novu.tenants.get("tenantIdentifier")
+```
+
+### Workflows
+
+- #### Create a new workflow
+
+```ts
+import { Novu, TemplateVariableTypeEnum, FilterPartTypeEnum, StepTypeEnum } from '@novu/node';
+
+const novu = new Novu('');
+
+// List all workflow groups
+const { data: workflowGroupsData } = await novu.notificationGroups.get();
+
+// Create a new workflow
+await novu.notificationTemplates.create({
+ name: 'Onboarding Workflow',
+ // taking first workflow group id
+ notificationGroupId: workflowGroupsData.data[0]._id,
+ steps: [
+ // Adding one chat step
+ {
+ active: true,
+ shouldStopOnFail: false,
+ // UUID is optional.
+ uuid: '78ab8c72-46de-49e4-8464-257085960f9e',
+ name: 'Chat',
+ filters: [
+ {
+ value: 'AND',
+ children: [
+ {
+ field: '{{chatContent}}',
+ value: 'flag',
+ operator: 'NOT_IN',
+ // 'payload'
+ on: FilterPartTypeEnum.PAYLOAD,
+ },
+ ],
+ },
+ ],
+ template: {
+ // 'chat'
+ type: StepTypeEnum.CHAT,
+ active: true,
+ subject: '',
+ variables: [
+ {
+ name: 'chatContent',
+ // 'String'
+ type: TemplateVariableTypeEnum.STRING,
+ required: true,
+ },
+ ],
+ content: '{{chatContent}}',
+ contentType: 'editor',
+ },
+ },
+ ],
+ description: 'Onboarding workflow to trigger after user sign up',
+ active: true,
+ draft: false,
+ critical: false,
+});
+```
+
+- #### Other Methods
+
+```ts
+import { Novu, TemplateVariableTypeEnum, FilterPartTypeEnum, StepTypeEnum } from '@novu/node';
+
+// update a workflow
+
+await novu.notificationTemplates.update("workflowId", {
+ name: "Send daily digest email update",
+ description: "This workflow will send daily digest email to user at 9:00 AM"
+ /**
+ * all other fields from create workflow payload
+ */
+})
+
+// get one workflow
+await novu.notificationTemplates.getOne("workflowId")
+
+// delete one workflow
+await novu.notificationTemplates.delete("workflowId")
+
+// update status of one workflow
+await novu.notificationTemplates.updateStatus("workflowId", false)
+
+// list all workflows
+await novu.notificationTemplates.getAll({
+ page: 0, // optional
+ limit: 20 // optional
+})
+```
diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts
index 7371be1c7be..64cbe3e9569 100644
--- a/packages/node/src/index.ts
+++ b/packages/node/src/index.ts
@@ -1,6 +1,9 @@
export {
ChatProviderIdEnum,
PushProviderIdEnum,
+ EmailProviderIdEnum,
+ SmsProviderIdEnum,
+ ProvidersIdEnum,
ChannelCTATypeEnum,
TemplateVariableTypeEnum,
IMessageTemplate,
diff --git a/packages/notification-center/src/i18n/languages/es.ts b/packages/notification-center/src/i18n/languages/es.ts
index ae0aeab949b..643acd1f9f5 100644
--- a/packages/notification-center/src/i18n/languages/es.ts
+++ b/packages/notification-center/src/i18n/languages/es.ts
@@ -3,10 +3,13 @@ import { ITranslationEntry } from '../lang';
export const ES: ITranslationEntry = {
translations: {
notifications: 'Notificaciones',
- markAllAsRead: 'marcar todo como leído',
+ markAllAsRead: 'Marcar todo como leído',
poweredBy: 'Con tecnología de',
settings: 'Configuración',
- noNewNotification: 'Nada nuevo que ver aquí todavía',
+ removeMessage: 'Eliminar mensaje',
+ markAsRead: 'Marcar como leído',
+ markAsUnRead: 'Marcar como no leído',
+ noNewNotification: 'Nada nuevo por aquí',
},
lang: 'es',
};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c53a76ce767..36604f72a1a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -174,6 +174,7 @@ importers:
'@nestjs/testing': ^10.2.2
'@novu/application-generic': ^0.19.0
'@novu/dal': ^0.19.0
+ '@novu/ee-auth': ^0.19.0
'@novu/node': ^0.19.0
'@novu/shared': ^0.19.0
'@novu/stateless': ^0.19.0
@@ -296,6 +297,8 @@ importers:
swagger-ui-express: 4.6.2_express@4.18.2
twilio: 4.15.0
uuid: 8.3.2
+ optionalDependencies:
+ '@novu/ee-auth': link:../../enterprise/packages/auth
devDependencies:
'@faker-js/faker': 6.3.1
'@nestjs/cli': 10.1.16
@@ -324,10 +327,14 @@ importers:
specifiers:
'@novu/application-generic': ^0.19.0
'@novu/shared': ^0.19.0
+ '@novu/testing': ^0.19.0
'@sentry/node': ^7.12.1
+ '@types/chai': ^4.2.11
'@types/express': ^4.17.8
'@types/html-to-text': ^9.0.1
+ '@types/mocha': ^8.2.3
'@types/node': ^14.14.6
+ '@types/sinon': ^9.0.0
'@types/smtp-server': ^3.5.7
bluebird: ^2.9.30
cross-env: ^7.0.3
@@ -338,12 +345,13 @@ importers:
languagedetect: ^1.1.1
lodash: ^4.17.15
mailparser: ^0.6.0
+ mocha: ^8.4.0
newrelic: ^9.15.0
- node-uuid: ^1.4.3
nodemon: ^2.0.7
prettier: ~2.8.0
rimraf: ^3.0.2
shelljs: ^0.8.5
+ sinon: ^9.2.4
smtp-server: ^1.4.0
spamc: 0.0.5
ts-jest: ^27.0.7
@@ -351,7 +359,8 @@ importers:
ts-node: ~10.9.1
tsconfig-paths: ~4.1.0
typescript: 4.9.5
- winston: ^1.0.0
+ uuid: ^9.0.0
+ winston: ^3.9.0
dependencies:
'@novu/application-generic': link:../../packages/application-generic
'@novu/shared': link:../../libs/shared
@@ -365,20 +374,26 @@ importers:
lodash: 4.17.21
mailparser: 0.6.2
newrelic: 9.15.0
- node-uuid: 1.4.8
rimraf: 3.0.2
shelljs: 0.8.5
smtp-server: 1.17.0
spamc: 0.0.5
- winston: 1.1.2
+ uuid: 9.0.0
+ winston: 3.10.0
devDependencies:
+ '@novu/testing': link:../../libs/testing
+ '@types/chai': 4.3.4
'@types/express': 4.17.17
'@types/html-to-text': 9.0.1
+ '@types/mocha': 8.2.3
'@types/node': 14.18.42
+ '@types/sinon': 9.0.11
'@types/smtp-server': 3.5.7
cross-env: 7.0.3
+ mocha: 8.4.0
nodemon: 2.0.22
prettier: 2.8.7
+ sinon: 9.2.4
ts-jest: 27.1.5_cnngzrja2umb46xxazlucyx2qu
ts-loader: 9.4.2_rggdtlzfqxxwxudp3onsqdyocm
ts-node: 10.9.1_wh2cnrlliuuxb2etxm2m3ttgna
@@ -1137,6 +1152,84 @@ importers:
stylelint-scss: 4.6.0_stylelint@15.10.1
typescript: 4.9.5
+ enterprise/packages/auth:
+ specifiers:
+ '@nestjs/common': '>=9.3.x'
+ '@nestjs/jwt': '>=9'
+ '@nestjs/passport': 9.0.3
+ '@novu/application-generic': ^0.19.0
+ '@novu/dal': ^0.19.0
+ '@novu/shared': ^0.19.0
+ '@types/chai': ^4.2.11
+ '@types/mocha': ^8.0.1
+ '@types/node': ^14.6.0
+ '@types/sinon': ^9.0.0
+ chai: ^4.2.0
+ cross-env: ^7.0.3
+ mocha: ^8.1.1
+ nodemon: ^2.0.3
+ passport: 0.6.0
+ passport-google-oauth: ^2.0.0
+ passport-oauth2: ^1.6.1
+ sinon: ^9.2.4
+ ts-node: ~10.9.1
+ typescript: 4.9.5
+ dependencies:
+ '@nestjs/common': 10.2.2_atc7tu2sld2m3nk4hmwkqn6qde
+ '@nestjs/jwt': 10.1.0_@nestjs+common@10.2.2
+ '@nestjs/passport': 9.0.3_kn4ljbedllcoqpuu4ifhphsdsu
+ '@novu/application-generic': link:../../../packages/application-generic
+ '@novu/dal': link:../../../libs/dal
+ '@novu/shared': link:../../../libs/shared
+ passport: 0.6.0
+ passport-google-oauth: 2.0.0
+ passport-oauth2: 1.7.0
+ devDependencies:
+ '@types/chai': 4.3.4
+ '@types/mocha': 8.2.3
+ '@types/node': 14.18.42
+ '@types/sinon': 9.0.11
+ chai: 4.3.7
+ cross-env: 7.0.3
+ mocha: 8.4.0
+ nodemon: 2.0.22
+ sinon: 9.2.4
+ ts-node: 10.9.1_wh2cnrlliuuxb2etxm2m3ttgna
+ typescript: 4.9.5
+
+ enterprise/packages/digest-schedule:
+ specifiers:
+ '@novu/shared': ^0.19.0
+ '@types/chai': ^4.2.11
+ '@types/mocha': ^8.0.1
+ '@types/node': ^14.6.0
+ '@types/sinon': ^9.0.0
+ chai: ^4.2.0
+ cross-env: ^7.0.3
+ date-fns: ^2.29.2
+ mocha: ^8.1.1
+ nodemon: ^2.0.3
+ rrule: ^2.7.2
+ sinon: ^9.2.4
+ ts-node: ~10.9.1
+ typescript: 4.9.5
+ dependencies:
+ '@novu/shared': link:../../../libs/shared
+ date-fns: 2.29.3
+ rrule: 2.7.2
+ devDependencies:
+ '@types/chai': 4.3.4
+ '@types/mocha': 8.2.3
+ '@types/node': 14.18.42
+ '@types/sinon': 9.0.11
+ chai: 4.3.7
+ cross-env: 7.0.3
+ mocha: 8.4.0
+ nodemon: 2.0.22
+ sinon: 9.2.4
+ ts-node: 10.9.1_wh2cnrlliuuxb2etxm2m3ttgna
+ typescript: 4.9.5
+
libs/dal:
specifiers:
'@aws-sdk/client-s3': ^3.382.0
@@ -1386,6 +1479,7 @@ importers:
'@istanbuljs/nyc-config-typescript': ^1.0.1
'@nestjs/common': '>=10'
'@nestjs/core': '>=10'
+ '@nestjs/jwt': ^10.1.0
'@nestjs/swagger': '>=6'
'@nestjs/terminus': '>=10'
'@nestjs/testing': '>=10'
@@ -1476,6 +1570,7 @@ importers:
'@google-cloud/storage': 6.9.5
'@nestjs/common': 10.2.2_j3td4gnlgk75ora6o6suo62byy
'@nestjs/core': 10.2.2_hvzojgbgemkkg4y2oz5vs6hq4y
+ '@nestjs/jwt': 10.1.0_@nestjs+common@10.2.2
'@nestjs/swagger': 7.1.9_yggpgkps2ewgemp53dklozvzx4
'@nestjs/terminus': 10.0.1_fav3sr7ld5p2uwyjvw6t25yci4
'@nestjs/testing': 10.2.2_h33h3l6i5mruhsbo3bha6vy2fi
@@ -3759,7 +3854,7 @@ packages:
engines: {node: '>=6.0.0'}
dependencies:
'@jridgewell/gen-mapping': 0.1.1
- '@jridgewell/trace-mapping': 0.3.18
+ '@jridgewell/trace-mapping': 0.3.19
dev: true
/@ampproject/remapping/2.2.1:
@@ -10865,6 +10960,14 @@ packages:
transitivePeerDependencies:
- supports-color
+ /@dabh/diagnostics/2.0.3:
+ resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
+ dependencies:
+ colorspace: 1.1.4
+ enabled: 2.0.0
+ kuler: 2.0.0
+ dev: false
+
/@design-systems/utils/2.12.0_zsjcj4gvi24ks76nprapl4hsmq:
resolution: {integrity: sha512-Y/d2Zzr+JJfN6u1gbuBUb1ufBuLMJJRZQk+dRmw8GaTpqKx5uf7cGUYGTwN02dIb3I+Tf+cW8jcGBTRiFxdYFg==}
peerDependencies:
@@ -13753,7 +13856,7 @@ packages:
'@jest/test-result': 29.5.0
'@jest/transform': 29.5.0
'@jest/types': 29.5.0
- '@jridgewell/trace-mapping': 0.3.18
+ '@jridgewell/trace-mapping': 0.3.19
'@types/node': 14.18.42
chalk: 4.1.2
collect-v8-coverage: 1.0.1
@@ -14974,7 +15077,7 @@ packages:
nopt: 5.0.0
npmlog: 5.0.1
rimraf: 3.0.2
- semver: 7.5.2
+ semver: 7.5.4
tar: 6.1.13
transitivePeerDependencies:
- encoding
@@ -15127,6 +15230,26 @@ packages:
- webpack-cli
dev: true
+ /@nestjs/common/10.2.2_atc7tu2sld2m3nk4hmwkqn6qde:
+ resolution: {integrity: sha512-TCOJK2K4FDT3GxFfURjngnjBewS/hizKNFSLBXtX4TTQm0dVQOtESnnVdP14sEiPM6suuWlrGnXW9UDqItGWiQ==}
+ peerDependencies:
+ class-transformer: '*'
+ class-validator: '*'
+ reflect-metadata: ^0.1.12
+ rxjs: ^7.1.0
+ peerDependenciesMeta:
+ class-transformer:
+ optional: true
+ class-validator:
+ optional: true
+ dependencies:
+ iterare: 1.2.1
+ reflect-metadata: 0.1.13
+ rxjs: 7.8.1
+ tslib: 2.6.2
+ uid: 2.0.2
+ dev: false
+
/@nestjs/common/10.2.2_j3td4gnlgk75ora6o6suo62byy:
resolution: {integrity: sha512-TCOJK2K4FDT3GxFfURjngnjBewS/hizKNFSLBXtX4TTQm0dVQOtESnnVdP14sEiPM6suuWlrGnXW9UDqItGWiQ==}
peerDependencies:
@@ -15354,6 +15477,16 @@ packages:
passport: 0.6.0
dev: false
+ /@nestjs/passport/9.0.3_kn4ljbedllcoqpuu4ifhphsdsu:
+ resolution: {integrity: sha512-HplSJaimEAz1IOZEu+pdJHHJhQyBOPAYWXYHfAPQvRqWtw4FJF1VXl1Qtk9dcXQX1eKytDtH+qBzNQc19GWNEg==}
+ peerDependencies:
+ '@nestjs/common': ^8.0.0 || ^9.0.0
+ passport: ^0.4.0 || ^0.5.0 || ^0.6.0
+ dependencies:
+ '@nestjs/common': 10.2.2_atc7tu2sld2m3nk4hmwkqn6qde
+ passport: 0.6.0
+ dev: false
+
/@nestjs/platform-express/10.2.2_h33h3l6i5mruhsbo3bha6vy2fi:
resolution: {integrity: sha512-g5AeXgPQrVm62JOl9FXk0w3Tq1tD4f6ouGikLYs/Aahy0q/Z2HNP9NjXZYpqcjHrpafPYnc3bfBuUwedKW1oHg==}
peerDependencies:
@@ -23114,6 +23247,7 @@ packages:
/@types/node/18.15.11:
resolution: {integrity: sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==}
+ dev: true
/@types/node/20.4.7:
resolution: {integrity: sha512-bUBrPjEry2QUTsnuEjzjbS7voGWCc30W0qzgMf90GPeDGFRakvrz47ju+oqDAKCXLUCe39u57/ORMl/O/04/9g==}
@@ -25411,10 +25545,6 @@ packages:
/async-validator/4.2.5:
resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==}
- /async/1.0.0:
- resolution: {integrity: sha512-5mO7DX4CbJzp9zjaFXusQQ4tzKJARjNB1Ih1pVBi8wkbmXy/xzIDgEMXxWePLzt2OdFwaxfneIlT1nCiXubrPQ==}
- dev: false
-
/async/2.6.4:
resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==}
dependencies:
@@ -27536,6 +27666,13 @@ packages:
resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
hasBin: true
+ /color/3.2.1:
+ resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==}
+ dependencies:
+ color-convert: 1.9.3
+ color-string: 1.9.1
+ dev: false
+
/color/4.2.3:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'}
@@ -27557,12 +27694,20 @@ packages:
/colors/1.0.3:
resolution: {integrity: sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==}
engines: {node: '>=0.1.90'}
+ dev: true
/colors/1.4.0:
resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==}
engines: {node: '>=0.1.90'}
dev: true
+ /colorspace/1.1.4:
+ resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==}
+ dependencies:
+ color: 3.2.1
+ text-hex: 1.0.0
+ dev: false
+
/columnify/1.6.0:
resolution: {integrity: sha512-lomjuFZKfM6MSAnV9aCZC9sc0qGbmZdfygNv+nCpqVkSKdCxCklLtd16O0EILGkImHw9ZpHkAnHaB+8Zxq5W6Q==}
engines: {node: '>=8.0.0'}
@@ -27768,7 +27913,7 @@ packages:
resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==}
/concat-map/0.0.1:
- resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=}
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
/concat-stream/1.6.2:
resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==}
@@ -29230,6 +29375,7 @@ packages:
/cycle/1.0.3:
resolution: {integrity: sha512-TVF6svNzeQCOpjCqsy0/CSy8VgObG3wXusJ73xW2GbG5rGx7lC8zxDSURicsXI2UsGdi2L0QNRCi745/wUDvsA==}
engines: {node: '>=0.4.0'}
+ dev: true
/cyclist/1.0.1:
resolution: {integrity: sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==}
@@ -30267,6 +30413,10 @@ packages:
resolution: {integrity: sha512-SNujglcLTTg+lDAcApPNgEdudaqQFiAbJCqzjNxJkvN9vAwCGi0uu8IUVvx+f16h+V44KCY6Y2yboroc9pilHg==}
dev: false
+ /enabled/2.0.0:
+ resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==}
+ dev: false
+
/encodeurl/1.0.2:
resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
engines: {node: '>= 0.8'}
@@ -31193,7 +31343,7 @@ packages:
object.values: 1.1.6
prop-types: 15.8.1
resolve: 2.0.0-next.4
- semver: 6.3.1
+ semver: 6.3.0
string.prototype.matchall: 4.0.8
/eslint-plugin-spellcheck/0.0.20_eslint@8.38.0:
@@ -31893,6 +32043,7 @@ packages:
/eyes/0.1.8:
resolution: {integrity: sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==}
engines: {node: '> 0.1.90'}
+ dev: true
/fast-copy/3.0.1:
resolution: {integrity: sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA==}
@@ -32450,6 +32601,10 @@ packages:
- encoding
dev: false
+ /fn.name/1.1.0:
+ resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
+ dev: false
+
/focus-lock/0.8.1:
resolution: {integrity: sha512-/LFZOIo82WDsyyv7h7oc0MJF9ACOvDRdx9rWPZ2pgMfNWu/z8hQDBtOchuB/0BVLmuFOZjV02YwUVzNsWx/EzA==}
engines: {node: '>=10'}
@@ -36846,7 +37001,7 @@ packages:
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
dependencies:
'@jest/types': 27.5.1
- '@types/node': 18.15.11
+ '@types/node': 14.18.42
chalk: 4.1.2
ci-info: 3.8.0
graceful-fs: 4.2.11
@@ -37282,7 +37437,7 @@ packages:
dev: false
/json-buffer/3.0.0:
- resolution: {integrity: sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=}
+ resolution: {integrity: sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==}
/json-buffer/3.0.1:
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
@@ -37596,6 +37751,10 @@ packages:
/known-css-properties/0.27.0:
resolution: {integrity: sha512-uMCj6+hZYDoffuvAJjFAPz56E9uoowFHmTkqRtRq5WyC5Q6Cu/fTZKNQpX/RbzChBYLLl3lo8CjFZBAZXq9qFg==}
+ /kuler/2.0.0:
+ resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==}
+ dev: false
+
/language-subtag-registry/0.3.22:
resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==}
@@ -40081,12 +40240,6 @@ packages:
/node-releases/2.0.13:
resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==}
- /node-uuid/1.4.8:
- resolution: {integrity: sha512-TkCET/3rr9mUuRp+CpO7qfgT++aAxfDRaalQhwPFzI9BY/2rCDn6OfpZOVggi1AXfTPpfkTrg5f5WQx5G1uLxA==}
- deprecated: Use uuid module instead
- hasBin: true
- dev: false
-
/nodemailer-fetch/1.6.0:
resolution: {integrity: sha512-P7S5CEVGAmDrrpn351aXOLYs1R/7fD5NamfMCHyi6WIkbjS2eeZUB/TkuvpOQr0bvRZicVqo59+8wbhR3yrJbQ==}
dev: false
@@ -40920,6 +41073,12 @@ packages:
dependencies:
wrappy: 1.0.2
+ /one-time/1.0.0:
+ resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==}
+ dependencies:
+ fn.name: 1.1.0
+ dev: false
+
/onesignal-node/3.4.0:
resolution: {integrity: sha512-9dNpfU5Xp6VhJLkdZT4kVqmOaU36RJOgp+6REQHyv+hLOcgqqa4/FRXxuHbjRCE51x9BK4jIC/gn2Mnw0gQgFQ==}
engines: {node: '>=8.13.0'}
@@ -41593,6 +41752,27 @@ packages:
passport-oauth2: 1.7.0
dev: false
+ /passport-google-oauth/2.0.0:
+ resolution: {integrity: sha512-JKxZpBx6wBQXX1/a1s7VmdBgwOugohH+IxCy84aPTZNq/iIPX6u7Mqov1zY7MKRz3niFPol0KJz8zPLBoHKtYA==}
+ engines: {node: '>= 0.4.0'}
+ dependencies:
+ passport-google-oauth1: 1.0.0
+ passport-google-oauth20: 2.0.0
+ dev: false
+
+ /passport-google-oauth1/1.0.0:
+ resolution: {integrity: sha512-qpCEhuflJgYrdg5zZIpAq/K3gTqa1CtHjbubsEsidIdpBPLkEVq6tB1I8kBNcH89RdSiYbnKpCBXAZXX/dtx1Q==}
+ dependencies:
+ passport-oauth1: 1.3.0
+ dev: false
+
+ /passport-google-oauth20/2.0.0:
+ resolution: {integrity: sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==}
+ engines: {node: '>= 0.4.0'}
+ dependencies:
+ passport-oauth2: 1.7.0
+ dev: false
+
/passport-jwt/4.0.1:
resolution: {integrity: sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==}
dependencies:
@@ -41600,6 +41780,15 @@ packages:
passport-strategy: 1.0.0
dev: false
+ /passport-oauth1/1.3.0:
+ resolution: {integrity: sha512-8T/nX4gwKTw0PjxP1xfD0QhrydQNakzeOpZ6M5Uqdgz9/a/Ag62RmJxnZQ4LkbdXGrRehQHIAHNAu11rCP46Sw==}
+ engines: {node: '>= 0.4.0'}
+ dependencies:
+ oauth: 0.9.15
+ passport-strategy: 1.0.0
+ utils-merge: 1.0.1
+ dev: false
+
/passport-oauth2/1.7.0:
resolution: {integrity: sha512-j2gf34szdTF2Onw3+76alNnaAExlUmHvkc7cL+cmaS5NzHzDP/BvFHJruueQ9XAeNOdpI+CH+PWid8RA7KCwAQ==}
engines: {node: '>= 0.4.0'}
@@ -41959,11 +42148,6 @@ packages:
dependencies:
find-up: 3.0.0
- /pkginfo/0.3.1:
- resolution: {integrity: sha512-yO5feByMzAp96LtP58wvPKSbaKAi/1C4kV9XpTctr6EepnP6F33RBNOiVrdz9BrPA98U2BMFsTNHo44TWcbQ2A==}
- engines: {node: '>= 0.4.0'}
- dev: false
-
/please-upgrade-node/3.2.0:
resolution: {integrity: sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==}
dependencies:
@@ -46114,7 +46298,7 @@ packages:
/rrule/2.7.2:
resolution: {integrity: sha512-NkBsEEB6FIZOZ3T8frvEBOB243dm46SPufpDckY/Ap/YH24V1zLeMmDY8OA10lk452NdrF621+ynDThE7FQU2A==}
dependencies:
- tslib: 2.5.0
+ tslib: 2.6.2
dev: false
/rsvp/4.8.5:
@@ -46645,6 +46829,7 @@ packages:
hasBin: true
dependencies:
lru-cache: 6.0.0
+ dev: true
/semver/7.5.4:
resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==}
@@ -48657,9 +48842,9 @@ packages:
uglify-js:
optional: true
dependencies:
- '@jridgewell/trace-mapping': 0.3.18
+ '@jridgewell/trace-mapping': 0.3.19
jest-worker: 27.5.1
- schema-utils: 3.1.2
+ schema-utils: 3.3.0
serialize-javascript: 6.0.1
terser: 5.19.3
webpack: 5.76.1
@@ -48681,9 +48866,9 @@ packages:
uglify-js:
optional: true
dependencies:
- '@jridgewell/trace-mapping': 0.3.18
+ '@jridgewell/trace-mapping': 0.3.19
jest-worker: 27.5.1
- schema-utils: 3.1.2
+ schema-utils: 3.3.0
serialize-javascript: 6.0.1
terser: 5.19.3
webpack: 5.78.0
@@ -48728,9 +48913,9 @@ packages:
uglify-js:
optional: true
dependencies:
- '@jridgewell/trace-mapping': 0.3.18
+ '@jridgewell/trace-mapping': 0.3.19
jest-worker: 27.5.1
- schema-utils: 3.1.2
+ schema-utils: 3.3.0
serialize-javascript: 6.0.1
terser: 5.19.3
webpack: 5.88.2
@@ -48751,10 +48936,10 @@ packages:
uglify-js:
optional: true
dependencies:
- '@jridgewell/trace-mapping': 0.3.18
+ '@jridgewell/trace-mapping': 0.3.19
'@swc/core': 1.3.49
jest-worker: 27.5.1
- schema-utils: 3.1.2
+ schema-utils: 3.3.0
serialize-javascript: 6.0.1
terser: 5.19.3
webpack: 5.88.2_@swc+core@1.3.49
@@ -48818,6 +49003,10 @@ packages:
engines: {node: '>=0.10'}
dev: true
+ /text-hex/1.0.0:
+ resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==}
+ dev: false
+
/text-table/0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
@@ -49214,7 +49403,6 @@ packages:
esbuild:
optional: true
dependencies:
- '@babel/core': 7.22.11
bs-logger: 0.2.6
fast-json-stable-stringify: 2.1.0
jest: 27.5.1_ts-node@10.9.1
@@ -51626,19 +51814,6 @@ packages:
triple-beam: 1.3.0
dev: false
- /winston/1.1.2:
- resolution: {integrity: sha512-rl9hA8se2gjdYI6nP1f+kjjSCFCZrObIJB/eXOcMdzWxxcYp7exyc5Bs248fwLT+wHA/+aK0VtBlPHL8qO0T0w==}
- engines: {node: '>= 0.8.0'}
- dependencies:
- async: 1.0.0
- colors: 1.0.3
- cycle: 1.0.3
- eyes: 0.1.8
- isstream: 0.1.2
- pkginfo: 0.3.1
- stack-trace: 0.0.10
- dev: false
-
/winston/2.4.7:
resolution: {integrity: sha512-vLB4BqzCKDnnZH9PHGoS2ycawueX4HLqENXQitvFHczhgW2vFpSOn31LZtVr1KU8YTw7DS4tM+cqyovxo8taVg==}
engines: {node: '>= 0.10.0'}
@@ -51651,6 +51826,23 @@ packages:
stack-trace: 0.0.10
dev: true
+ /winston/3.10.0:
+ resolution: {integrity: sha512-nT6SIDaE9B7ZRO0u3UvdrimG0HkB7dSTAgInQnNR2SOPJ4bvq5q79+pXLftKmP52lJGW15+H5MCK0nM9D3KB/g==}
+ engines: {node: '>= 12.0.0'}
+ dependencies:
+ '@colors/colors': 1.5.0
+ '@dabh/diagnostics': 2.0.3
+ async: 3.2.4
+ is-stream: 2.0.1
+ logform: 2.5.1
+ one-time: 1.0.0
+ readable-stream: 3.6.2
+ safe-stable-stringify: 2.4.3
+ stack-trace: 0.0.10
+ triple-beam: 1.3.0
+ winston-transport: 4.5.0
+ dev: false
+
/word-wrap/1.2.3:
resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==}
engines: {node: '>=0.10.0'}