diff --git a/backend/core-api/src/connectionResolvers.ts b/backend/core-api/src/connectionResolvers.ts index ee80ed738d..f08a646c02 100644 --- a/backend/core-api/src/connectionResolvers.ts +++ b/backend/core-api/src/connectionResolvers.ts @@ -130,6 +130,15 @@ import { loadSegmentClass, } from './modules/segments/db/models/Segments'; +import { + IAutomationDocument, + IAutomationExecutionDocument, +} from 'erxes-api-shared/core-modules'; +import { + IEmailDeliveryModel, + loadEmailDeliveryClass, +} from '~/modules/organization/team-member/db/models/EmailDeliveries'; +import { IEmailDeliveriesDocument } from '~/modules/organization/team-member/types'; import { IAutomationModel, loadClass as loadAutomationClass, @@ -138,10 +147,6 @@ import { IExecutionModel, loadClass as loadExecutionClass, } from './modules/automations/db/models/Executions'; -import { - IAutomationDocument, - IAutomationExecutionDocument, -} from 'erxes-api-shared/core-modules'; import { ILogModel, loadLogsClass } from './modules/logs/db/models/Logs'; export interface IModels { @@ -176,6 +181,7 @@ export interface IModels { Documents: IDocumentModel; Automations: IAutomationModel; AutomationExecutions: IExecutionModel; + EmailDeliveries: IEmailDeliveryModel; Logs: ILogModel; } @@ -326,6 +332,11 @@ export const loadClasses = ( IExecutionModel >('automations_executions', loadExecutionClass(models)); + models.EmailDeliveries = db.model< + IEmailDeliveriesDocument, + IEmailDeliveryModel + >('email_deliveries', loadEmailDeliveryClass(models)); + const db_name = db.name; const logDb = db.useDb(`${db_name}_logs`); diff --git a/backend/core-api/src/modules/contacts/graphql/schemas/customer.ts b/backend/core-api/src/modules/contacts/graphql/schemas/customer.ts index c77e647721..291330205e 100644 --- a/backend/core-api/src/modules/contacts/graphql/schemas/customer.ts +++ b/backend/core-api/src/modules/contacts/graphql/schemas/customer.ts @@ -75,7 +75,7 @@ const queryParams = ` segment: String type: String ids: [String] - excludeIds: Boolean + excludeIds: [String] tagIds: [String] excludeTagIds: [String] diff --git a/backend/core-api/src/modules/contacts/utils.ts b/backend/core-api/src/modules/contacts/utils.ts index 168a365952..04cef82cd0 100644 --- a/backend/core-api/src/modules/contacts/utils.ts +++ b/backend/core-api/src/modules/contacts/utils.ts @@ -13,7 +13,9 @@ export const generateFilter = async (params: any, models: IModels) => { brandIds, integrationIds, integrationTypes, - status + status, + ids, + excludeIds, } = params; const filter = {}; @@ -30,6 +32,19 @@ export const generateFilter = async (params: any, models: IModels) => { filter['searchText'] = { $regex: searchValue, $options: 'i' }; } + if (ids?.length || excludeIds?.length) { + if (ids?.length && excludeIds?.length) { + filter['_id'] = { + $in: ids, + $nin: excludeIds, + }; + } else if (ids?.length) { + filter['_id'] = { $in: ids }; + } else if (excludeIds?.length) { + filter['_id'] = { $nin: excludeIds }; + } + } + if (brandIds || integrationIds || integrationTypes) { const relatedIntegrationIdSet = new Set(); diff --git a/backend/core-api/src/modules/organization/team-member/constants.ts b/backend/core-api/src/modules/organization/team-member/constants.ts new file mode 100644 index 0000000000..198695126c --- /dev/null +++ b/backend/core-api/src/modules/organization/team-member/constants.ts @@ -0,0 +1,5 @@ +export const EMAIL_DELIVERY_STATUS = { + PENDING: 'pending', + RECEIVED: 'received', + ALL: ['pending', 'received'], +}; diff --git a/backend/core-api/src/modules/organization/team-member/db/definitions/emailDeliveries.ts b/backend/core-api/src/modules/organization/team-member/db/definitions/emailDeliveries.ts new file mode 100644 index 0000000000..f956af60a2 --- /dev/null +++ b/backend/core-api/src/modules/organization/team-member/db/definitions/emailDeliveries.ts @@ -0,0 +1,19 @@ +import { mongooseStringRandomId } from 'erxes-api-shared/utils'; +import { Schema } from 'mongoose'; +import { EMAIL_DELIVERY_STATUS } from '~/modules/organization/team-member/constants'; + +export const emailDeliveriesSchema = new Schema({ + _id: mongooseStringRandomId, + subject: { type: String }, + body: { type: String }, + to: { type: [String] }, + cc: { type: [String], optional: true }, + bcc: { type: [String], optional: true }, + attachments: { type: [Object] }, + from: { type: String }, + kind: { type: String }, + customerId: { type: String }, + userId: { type: String }, + createdAt: { type: Date, default: Date.now }, + status: { type: String, enum: EMAIL_DELIVERY_STATUS.ALL }, +}); diff --git a/backend/core-api/src/modules/organization/team-member/db/models/EmailDeliveries.ts b/backend/core-api/src/modules/organization/team-member/db/models/EmailDeliveries.ts new file mode 100644 index 0000000000..bf4fe957de --- /dev/null +++ b/backend/core-api/src/modules/organization/team-member/db/models/EmailDeliveries.ts @@ -0,0 +1,33 @@ +import { Model } from 'mongoose'; +import { IModels } from '~/connectionResolvers'; +import { emailDeliveriesSchema } from '~/modules/organization/team-member/db/definitions/emailDeliveries'; +import { + IEmailDeliveries, + IEmailDeliveriesDocument, +} from '~/modules/organization/team-member/types'; + +export interface IEmailDeliveryModel extends Model { + createEmailDelivery(doc: IEmailDeliveries): Promise; + updateEmailDeliveryStatus(_id: string, status: string): Promise; +} + +export const loadEmailDeliveryClass = (models: IModels) => { + class EmailDelivery { + /** + * Create an EmailDelivery document + */ + public static async createEmailDelivery(doc: IEmailDeliveries) { + return models.EmailDeliveries.create({ + ...doc, + }); + } + + public static async updateEmailDeliveryStatus(_id: string, status: string) { + return models.EmailDeliveries.updateOne({ _id }, { $set: { status } }); + } + } + + emailDeliveriesSchema.loadClass(EmailDelivery); + + return emailDeliveriesSchema; +}; diff --git a/backend/core-api/src/modules/organization/team-member/db/models/Users.ts b/backend/core-api/src/modules/organization/team-member/db/models/Users.ts index 0ef6aa6b72..1defa9c0d3 100644 --- a/backend/core-api/src/modules/organization/team-member/db/models/Users.ts +++ b/backend/core-api/src/modules/organization/team-member/db/models/Users.ts @@ -1,26 +1,26 @@ import * as bcrypt from 'bcryptjs'; +import * as crypto from 'crypto'; import * as jwt from 'jsonwebtoken'; import { Model } from 'mongoose'; -import * as crypto from 'crypto'; -import { redis } from 'erxes-api-shared/utils'; import { USER_ROLES, - userSchema, userMovemmentSchema, + userSchema, } from 'erxes-api-shared/core-modules'; +import { redis } from 'erxes-api-shared/utils'; import { saveValidatedToken } from '@/auth/utils'; -import { IModels } from '~/connectionResolvers'; import { - IUser, + IAppDocument, IDetail, + IEmailSignature, ILink, - IUserMovementDocument, + IUser, IUserDocument, - IEmailSignature, - IAppDocument, + IUserMovementDocument, } from 'erxes-api-shared/core-types'; +import { IModels } from '~/connectionResolvers'; import { USER_MOVEMENT_STATUSES } from 'erxes-api-shared/core-modules'; @@ -322,6 +322,10 @@ export const loadUserClass = (models: IModels) => { // Checking duplicated email await models.Users.checkDuplication({ email }); + if (!(await models.UsersGroups.findOne({ _id: groupId }))) { + throw new Error('Invalid group'); + } + const { token, expires } = await User.generateToken(); this.checkPassword(password); diff --git a/backend/core-api/src/modules/organization/team-member/graphql/mutations.ts b/backend/core-api/src/modules/organization/team-member/graphql/mutations.ts index e2e47ece3f..1b067e7642 100644 --- a/backend/core-api/src/modules/organization/team-member/graphql/mutations.ts +++ b/backend/core-api/src/modules/organization/team-member/graphql/mutations.ts @@ -1,10 +1,13 @@ -import { IContext } from '~/connectionResolvers'; import { - IUser, IDetail, - ILink, IEmailSignature, + ILink, + IUser, } from 'erxes-api-shared/core-types'; +import { isEnabled, sendTRPCMessage } from 'erxes-api-shared/utils'; +import { IContext } from '~/connectionResolvers'; +import { sendInvitationEmail } from '~/modules/organization/team-member/utils'; +import { resetPermissionsCache } from '~/modules/permissions/utils'; export interface IUsersEdit extends IUser { channelIds?: string[]; @@ -198,7 +201,7 @@ export const userMutations = { departmentId?: string; }>; }, - { models }: IContext, + { models, subdomain }: IContext, ) { for (const entry of entries) { await models.Users.checkDuplication({ email: entry.email }); @@ -210,6 +213,8 @@ export const userMutations = { if (docModified?.scopeBrandIds?.length) { doc.brandIds = docModified.scopeBrandIds; } + + const token = await models.Users.invite(doc); const createdUser = await models.Users.findOne({ email: entry.email }); if (entry.branchId) { @@ -229,7 +234,21 @@ export const userMutations = { }, ); } + + if (entry.channelIds && (await isEnabled('frontline'))) { + sendTRPCMessage({ + pluginName: 'frontline', + method: 'mutation', + module: 'inbox', + action: 'updateUserChannels', + input: { channelIds: entry.channelIds, userId: createdUser?._id }, + }); + } + + sendInvitationEmail(models, subdomain, { email: entry.email, token }); } + + await resetPermissionsCache(models); }, /* diff --git a/backend/core-api/src/modules/organization/team-member/types.ts b/backend/core-api/src/modules/organization/team-member/types.ts new file mode 100644 index 0000000000..0dd22f4bec --- /dev/null +++ b/backend/core-api/src/modules/organization/team-member/types.ts @@ -0,0 +1,39 @@ +import { Document } from 'mongoose'; + +export interface IAttachmentParams { + data: string; + filename: string; + size: number; + mimeType: string; +} + +export interface IEmailDeliveries { + subject: string; + body: string; + to: string[]; + cc?: string[]; + bcc?: string[]; + attachments?: IAttachmentParams[]; + from: string; + kind: string; + userId?: string; + customerId?: string; + status?: string; +} + +export interface IEmailDeliveriesDocument extends IEmailDeliveries, Document { + id: string; +} + +export interface IEmailParams { + toEmails?: string[]; + fromEmail?: string; + title?: string; + customHtml?: string; + customHtmlData?: any; + template?: { name?: string; data?: any }; + attachments?: object[]; + modifier?: (data: any, email: string) => void; + transportMethod?: string; + getOrganizationDetail?: ({ subdomain }: { subdomain: string }) => any; +} diff --git a/backend/core-api/src/modules/organization/team-member/utils.ts b/backend/core-api/src/modules/organization/team-member/utils.ts new file mode 100644 index 0000000000..40106400c0 --- /dev/null +++ b/backend/core-api/src/modules/organization/team-member/utils.ts @@ -0,0 +1,292 @@ +import * as AWS from 'aws-sdk'; +import { getEnv } from 'erxes-api-shared/utils'; +import fs from 'fs'; +import * as Handlebars from 'handlebars'; +import * as nodemailer from 'nodemailer'; +import path from 'path'; +import { IModels } from '~/connectionResolvers'; +import { getConfig } from '~/modules/organization/settings/utils/configs'; +import { IEmailParams } from '~/modules/organization/team-member/types'; + +/** + * Create default or ses transporter + */ +export const createTransporter = async ({ ses }, models?: IModels) => { + if (ses) { + const AWS_SES_ACCESS_KEY_ID = await getConfig( + 'AWS_SES_ACCESS_KEY_ID', + '', + models, + ); + const AWS_SES_SECRET_ACCESS_KEY = await getConfig( + 'AWS_SES_SECRET_ACCESS_KEY', + '', + models, + ); + const AWS_REGION = await getConfig('AWS_REGION', '', models); + + AWS.config.update({ + region: AWS_REGION, + accessKeyId: AWS_SES_ACCESS_KEY_ID, + secretAccessKey: AWS_SES_SECRET_ACCESS_KEY, + }); + + return nodemailer.createTransport({ + SES: new AWS.SES({ apiVersion: '2010-12-01' }), + }); + } + + const MAIL_SERVICE = await getConfig('MAIL_SERVICE', '', models); + const MAIL_PORT = await getConfig('MAIL_PORT', '', models); + const MAIL_USER = await getConfig('MAIL_USER', '', models); + const MAIL_PASS = await getConfig('MAIL_PASS', '', models); + const MAIL_HOST = await getConfig('MAIL_HOST', '', models); + + let auth; + + if (MAIL_USER && MAIL_PASS) { + auth = { + user: MAIL_USER, + pass: MAIL_PASS, + }; + } + + return nodemailer.createTransport({ + service: MAIL_SERVICE, + host: MAIL_HOST, + port: MAIL_PORT, + auth, + }); +}; + +/** + * Read contents of a file + */ +export const readFile = async (filename: string) => { + const filePath = path.resolve( + __dirname, + `../../../private/emailTemplates/${filename}.html`, + ); + return fs.promises.readFile(filePath, 'utf8'); +}; + +/** + * Apply template + */ +const applyTemplate = async (data: any, templateName: string) => { + let template: any = await readFile(templateName); + + template = Handlebars.compile(template.toString()); + + return template(data); +}; + +export const sendEmail = async ( + subdomain: string, + params: IEmailParams, + models?: IModels, +) => { + const { + toEmails = [], + fromEmail, + title, + customHtml, + customHtmlData, + template = {}, + modifier, + attachments, + getOrganizationDetail, + transportMethod, + } = params; + + const NODE_ENV = getEnv({ name: 'NODE_ENV' }); + const DEFAULT_EMAIL_SERVICE = await getConfig( + 'DEFAULT_EMAIL_SERVICE', + 'SES', + models, + ); + const defaultTemplate = await getConfig('COMPANY_EMAIL_TEMPLATE', '', models); + const defaultTemplateType = await getConfig( + 'COMPANY_EMAIL_TEMPLATE_TYPE', + '', + models, + ); + const COMPANY_EMAIL_FROM = await getConfig('COMPANY_EMAIL_FROM', '', models); + const AWS_SES_CONFIG_SET = await getConfig('AWS_SES_CONFIG_SET', '', models); + const AWS_SES_ACCESS_KEY_ID = await getConfig( + 'AWS_SES_ACCESS_KEY_ID', + '', + models, + ); + const AWS_SES_SECRET_ACCESS_KEY = await getConfig( + 'AWS_SES_SECRET_ACCESS_KEY', + '', + models, + ); + + const DOMAIN = getEnv({ name: 'DOMAIN', subdomain }); + + const VERSION = getEnv({ name: 'VERSION' }); + + // do not send email it is running in test mode + if (NODE_ENV === 'test') { + return; + } + + // try to create transporter or throw configuration error + let transporter; + let sendgridMail; + + try { + transporter = await createTransporter( + { ses: DEFAULT_EMAIL_SERVICE === 'SES' }, + models, + ); + + if (transportMethod === 'sendgrid' || (VERSION && VERSION === 'saas')) { + sendgridMail = require('@sendgrid/mail'); + + const SENDGRID_API_KEY = getEnv({ name: 'SENDGRID_API_KEY', subdomain }); + + sendgridMail.setApiKey(SENDGRID_API_KEY); + } + } catch (e) { + // return debugError(e.message); + console.log(e); + return; + } + + const { data = {}, name } = template; + + // for unsubscribe url + data.domain = DOMAIN; + + let hasCompanyFromEmail = COMPANY_EMAIL_FROM && COMPANY_EMAIL_FROM.length > 0; + + if (models && subdomain && getOrganizationDetail) { + const organization = await getOrganizationDetail({ subdomain }); + + if (organization.isWhiteLabel) { + data.whiteLabel = true; + data.organizationName = organization.name || ''; + data.organizationDomain = organization.domain || ''; + + hasCompanyFromEmail = true; + } else { + hasCompanyFromEmail = false; + } + } + + for (const toEmail of toEmails) { + if (modifier) { + modifier(data, toEmail); + } + + // generate email content by given template + let html; + + if (name) { + html = await applyTemplate(data, name); + } else if ( + !defaultTemplate || + !defaultTemplateType || + (defaultTemplateType && defaultTemplateType.toString() === 'simple') + ) { + html = await applyTemplate(data, 'base'); + } else if (defaultTemplate) { + html = Handlebars.compile(defaultTemplate.toString())(data || {}); + } + + if (customHtml) { + html = Handlebars.compile(customHtml)(customHtmlData || {}); + } + + const mailOptions: any = { + from: + fromEmail || + (hasCompanyFromEmail + ? `Noreply <${COMPANY_EMAIL_FROM}>` + : 'noreply@erxes.io'), + to: toEmail, + subject: title, + html, + attachments, + }; + + if (!mailOptions.from) { + throw new Error(`"From" email address is missing: ${mailOptions.from}`); + } + + let headers: { [key: string]: string } = {}; + + if (models && subdomain) { + const emailDelivery = (await models.EmailDeliveries.createEmailDelivery({ + kind: 'transaction', + to: [toEmail], + from: mailOptions.from, + subject: title || '', + body: html, + status: 'pending', + })) as any; + + headers = { + 'X-SES-CONFIGURATION-SET': AWS_SES_CONFIG_SET || 'erxes', + EmailDeliveryId: emailDelivery && emailDelivery._id, + }; + } + + if (AWS_SES_ACCESS_KEY_ID && AWS_SES_SECRET_ACCESS_KEY) { + headers['X-SES-CONFIGURATION-SET'] = AWS_SES_CONFIG_SET || 'erxes-saas'; + } + + mailOptions.headers = headers; + + try { + if (sendgridMail) { + await sendgridMail.send(mailOptions).catch((error) => { + console.error(error); + + if (error.response) { + console.error(error.response.body); + } + }); + } else { + await transporter.sendMail(mailOptions); + } + } catch (e) { + // debugError(`Error sending email: ${e.message}`); + console.log(`Error sending email: ${e.message}`); + } + } +}; + +export const sendInvitationEmail = ( + models: IModels, + subdomain: string, + { + email, + token, + }: { + email: string; + token: string; + }, +) => { + const DOMAIN = getEnv({ name: 'DOMAIN', subdomain }); + const confirmationUrl = `${DOMAIN}/confirmation?token=${token}`; + + sendEmail( + subdomain, + { + toEmails: [email], + title: 'Team member invitation', + template: { + name: 'userInvitation', + data: { + content: confirmationUrl, + domain: DOMAIN, + }, + }, + }, + models, + ); +}; diff --git a/backend/core-api/src/private/emailTemplates/base.html b/backend/core-api/src/private/emailTemplates/base.html new file mode 100644 index 0000000000..edb5435114 --- /dev/null +++ b/backend/core-api/src/private/emailTemplates/base.html @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + +
+
+ + + + + + +
+ {{{ content }}} +
+
+
+ + \ No newline at end of file diff --git a/backend/core-api/src/private/emailTemplates/conversationCron.html b/backend/core-api/src/private/emailTemplates/conversationCron.html new file mode 100644 index 0000000000..3ac2bb994a --- /dev/null +++ b/backend/core-api/src/private/emailTemplates/conversationCron.html @@ -0,0 +1,368 @@ + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+
+ + + + + + + + +
+ + + + + + + +
+ +
+
+
{{brand.name}}
+ +
+
+ +
+
+ + +
+ + + + + + +
+ +
+ + + + + +
+ + + + + +
+ + + + + + +
+

{{{question.content}}}

+
+ + + + + + + + +
+ {{customer.name}} + {{question.createdAt}}
+
+ {{#each answers}} + + + + + +
+ + + + +
+

{{{content}}}

+
+ + + + + + + + + + +
{{createdAt}} + {{user.fullName}} +
+
+ +
+ {{/each}} +
+
+ +
+
+ +
+ + + + + + + + + + + + + +
+ +
+ +
+ Copyright © + erxes Inc. All rights + reserved. +
+ + + + + + \ No newline at end of file diff --git a/backend/core-api/src/private/emailTemplates/invitation.html b/backend/core-api/src/private/emailTemplates/invitation.html new file mode 100644 index 0000000000..5fad0c0366 --- /dev/null +++ b/backend/core-api/src/private/emailTemplates/invitation.html @@ -0,0 +1,156 @@ + + + + + + + + + +
+
+ + + + + + + + +
+ +
+
+
+
+

+ Username: {{ username }}
+

+

+ Password: {{ password }}
+

+
+
+ + + + + + + + + + + + + +
+ +
+ +
+ Copyright © + erxes Inc. All rights + reserved. +
+ + \ No newline at end of file diff --git a/backend/core-api/src/private/emailTemplates/magicLogin.html b/backend/core-api/src/private/emailTemplates/magicLogin.html new file mode 100644 index 0000000000..3cd3123b12 --- /dev/null +++ b/backend/core-api/src/private/emailTemplates/magicLogin.html @@ -0,0 +1,288 @@ + + + + + + +
+
+
+ + + + + + + + +
+ + + + + + + +
+
+

+ Click the button below to log in to erxes. The log in link will + expire in 10 minutes.
+

+ + + + + + + +
+ + Log in to erxes + +
+ +

+ This email contains private information for your erxes account — + please don’t forward it. Confirming this request will securely log + you in using {{{ email }}}. +

+ +
+
+
+ + + + + + + + + + + + + +
+ +
+ +
+ Copyright © + erxes Inc. All rights reserved. +
+ + diff --git a/backend/core-api/src/private/emailTemplates/notification.html b/backend/core-api/src/private/emailTemplates/notification.html new file mode 100644 index 0000000000..36a0e1b26c --- /dev/null +++ b/backend/core-api/src/private/emailTemplates/notification.html @@ -0,0 +1,183 @@ + + + + + + +
+
+
+ + + + + + + + +
+ +
+
+ +

+ {{ notification.title }} +

+
+

{{{ userName }}} {{{ action }}}:

+
+ {{{ notification.content }}} +
+

{{ notification.date }}

+
+ + + + + + + +
+ + View notification + + +
+
or click the link
+
+
+ +
+
+ + + + + + + + + + +
+ +
+ Copyright © + erxes Inc. All rights + reserved. +
+ + diff --git a/backend/core-api/src/private/emailTemplates/resetPassword.html b/backend/core-api/src/private/emailTemplates/resetPassword.html new file mode 100644 index 0000000000..6b87d8619a --- /dev/null +++ b/backend/core-api/src/private/emailTemplates/resetPassword.html @@ -0,0 +1,183 @@ + + + + + + +
+
+
+ + + + + + + + +
+ +
+
+

+ You recently requested a password reset. Click the link below to continue.
+

+ + + + + + + +
+ + Reset password + +
+ +

+
or click the link below
+ {{{ content }}} +

+
+
+
+ + + + + + + + + + + + + +
+ +
+ +
+ Copyright © + erxes Inc. All rights + reserved. +
+ + diff --git a/backend/core-api/src/private/emailTemplates/unsubscribe.html b/backend/core-api/src/private/emailTemplates/unsubscribe.html new file mode 100644 index 0000000000..18e886320d --- /dev/null +++ b/backend/core-api/src/private/emailTemplates/unsubscribe.html @@ -0,0 +1,146 @@ + + + + + + + + + +
+ +

Unsubscribe Successful

+
+ You will no longer receive email from erxes. +
+ « return to our website +
+ + + + + + + + + + + + + +
+ +
+ +
+ Copyright © + erxes Inc. All + rights reserved. +
+ + + diff --git a/backend/core-api/src/private/emailTemplates/userInvitation.html b/backend/core-api/src/private/emailTemplates/userInvitation.html new file mode 100644 index 0000000000..d06fd7a55f --- /dev/null +++ b/backend/core-api/src/private/emailTemplates/userInvitation.html @@ -0,0 +1,268 @@ + + + + + + + + + + + +
+
+ + + + + + +
+
+ + + + + + + + +
+ +
+
+
+
+
+ + + + + + +
+
+

+ You have been invited to become team member of + {{{ domain }}}. + Click the button below to continue.
+

+
+ + Click here + +
+
+ Or paste this link
+ {{ content }} +
+
+
+ + + + + + + + + + + + + +
+ +
+ +
+ Copyright © + erxes Inc. All + rights reserved. +
+
+ + + diff --git a/backend/core-api/src/private/version.json b/backend/core-api/src/private/version.json new file mode 100644 index 0000000000..35c2290574 --- /dev/null +++ b/backend/core-api/src/private/version.json @@ -0,0 +1 @@ +{"packageVersion":"0.17.6","sha":"b91386fe3b15d1fbffdc06e265b41875af3c3831","abbreviatedSha":"b91386fe3b","branch":"develop","tag":null,"committer":"Munkh-Orgil ","committerDate":"2020-08-28T07:46:23.000Z","author":"Munkh-Orgil ","authorDate":"2020-08-28T07:46:23.000Z","commitMessage":"erxes/erxes#2253","root":"/home/munkhjin/Documents/workspace/erxes/erxes-api","commonGitDir":"/home/munkhjin/Documents/workspace/erxes/erxes-api/.git","worktreeGitDir":"/home/munkhjin/Documents/workspace/erxes/erxes-api/.git","lastTag":null,"commitsSinceLastTag":null,"parents":["026ff2dd577d52ab010c5044ea88e022a347ec81"]} \ No newline at end of file diff --git a/package.json b/package.json index d631d8aa6f..754112dc2d 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "graphql-subscriptions": "^3.0.0", "graphql-tag": "^2.12.6", "graphql-ws": "^5.16.0", + "handlebars": "^4.7.8", "http-proxy-middleware": "^3.0.3", "i18next": "^23.16.4", "i18next-browser-languagedetector": "^8.0.0",