Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
dd9b203
tenant update migs start
epipav Dec 5, 2022
d287208
tenant plan up migrations
epipav Dec 5, 2022
b1440b4
plans model updated
epipav Dec 5, 2022
654f198
feature flagging for eagle eye, chc pro and automations
epipav Dec 6, 2022
310ef1c
downgrade expired trial plans job
epipav Dec 6, 2022
aec1250
trial ends at date check
epipav Dec 6, 2022
b283062
formatting
epipav Dec 6, 2022
0295cdc
formatting
epipav Dec 6, 2022
39d679b
plans updated in all project
epipav Dec 6, 2022
074471d
formatting
epipav Dec 6, 2022
3083a7d
posthog not initialized if key is not present
epipav Dec 6, 2022
07009d0
formatting
epipav Dec 7, 2022
d93a716
tests are passing
epipav Dec 7, 2022
2c77407
fix package json typo
epipav Dec 7, 2022
268f13c
formatting
epipav Dec 7, 2022
6537a6f
posthog env key added to dist local
epipav Dec 7, 2022
f5c0089
permissions and 403 errors, plan script also allows multiple tenants now
epipav Dec 7, 2022
33e61a4
fixes for identifyTenant after rebasing
epipav Dec 7, 2022
8d0f4ad
enums for feature flags
epipav Dec 7, 2022
8b8df65
Merge branch 'main' into feature/tenant-plans-and-feature-flagging
epipav Dec 7, 2022
9f1e766
edition enums
epipav Dec 7, 2022
6f1c9c9
enterprise name update
epipav Dec 7, 2022
818738a
migrations update plan before altering column
epipav Dec 8, 2022
aa8d008
automation count as text because of posthog bug
epipav Dec 9, 2022
04489ba
posthog flush queue after setting tenant properties immediately
epipav Dec 9, 2022
ed7914f
waiting 100ms before returning in automation create and destroy
epipav Dec 9, 2022
fb48336
ensuring flags before returning for realtime use cases
epipav Dec 9, 2022
958b2e1
lint fix
epipav Dec 9, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backend/.env.dist.local
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ CROWD_COMPREHEND_AWS_REGION=
# Segment settings
CROWD_SEGMENT_WRITE_KEY=TdX3BLaZuHpHyzN2lcDiNiRHDSH9Piyl

# Posthog settings
CROWD_POSTHOG_API_KEY=

SUPERFACE_SDK_TOKEN=sfs_29f8c03402d48b583f9b35bbb5fbb423cf638e73bde38a7cce6a5fb7bb352720193471189882b6d82494203bc998be47a52ab9d4250f90b16d2bd86b4c7ace2b_1d8a819b

# Netlify settings
Expand Down
3 changes: 3 additions & 0 deletions backend/config/custom-environment-variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@
"segment": {
"writeKey": "CROWD_SEGMENT_WRITE_KEY"
},
"posthog": {
"apiKey": "CROWD_POSTHOG_API_KEY"
},
"comprehend": {
"aws": {
"accountId": "CROWD_COMPREHEND_AWS_ACCOUNT_ID",
Expand Down
1 change: 1 addition & 0 deletions backend/config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"cubejs": {},
"searchEngine": {},
"segment": {},
"posthog": {},
"comprehend": {
"aws": {}
},
Expand Down
20 changes: 20 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"stripe:start": "stripe listen --forward-to localhost:8080/api/plan/stripe/webhook",
"lint": "npx eslint .",
"format": "npx prettier --write .",
"script:process-integration": "SERVICE=script ts-node ./src/bin/scripts/process-integration.ts"
"script:process-integration": "SERVICE=script ts-node ./src/bin/scripts/process-integration.ts",
"script:change-tenant-plan": "SERVICE=script ts-node ./src/bin/scripts/change-tenant-plan.ts"
},
"dependencies": {
"@aws-sdk/client-comprehend": "^3.159.0",
Expand Down Expand Up @@ -80,6 +81,7 @@
"passport-slack": "0.0.7",
"pg": "^8.7.3",
"pm2": "^5.2.0",
"posthog-node": "^2.2.3",
"redis": "^4.5.0",
"sanitize-html": "^2.7.1",
"sequelize": "6.21.2",
Expand Down
13 changes: 13 additions & 0 deletions backend/src/api/automation/automationCreate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import PermissionChecker from '../../services/user/permissionChecker'
import Permissions from '../../security/permissions'
import AutomationService from '../../services/automationService'
import track from '../../segment/track'
import identifyTenant from '../../segment/identifyTenant'
import { FeatureFlag } from '../../types/common'
import ensureFlagUpdated from '../../feature-flags/ensureFlagUpdated'
import AutomationRepository from '../../database/repositories/automationRepository'

/**
* POST /tenant/{tenantId}/automation
Expand All @@ -19,9 +23,18 @@ import track from '../../segment/track'
*/
export default async (req, res) => {
new PermissionChecker(req).validateHas(Permissions.values.automationCreate)

const payload = await new AutomationService(req).create(req.body.data)

track('Automation Created', { ...payload }, { ...req })

identifyTenant(req)

const automationCount = await AutomationRepository.countAll(req.database, req.currentTenant.id)
await ensureFlagUpdated(FeatureFlag.AUTOMATIONS, req.currentTenant.id, req.posthog, {
plan: req.currentTenant.plan,
automationCount,
})

await req.responseHandler.success(req, res, payload)
}
13 changes: 13 additions & 0 deletions backend/src/api/automation/automationDestroy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import PermissionChecker from '../../services/user/permissionChecker'
import Permissions from '../../security/permissions'
import AutomationService from '../../services/automationService'
import track from '../../segment/track'
import identifyTenant from '../../segment/identifyTenant'
import ensureFlagUpdated from '../../feature-flags/ensureFlagUpdated'
import { FeatureFlag } from '../../types/common'
import AutomationRepository from '../../database/repositories/automationRepository'

/**
* DELETE /tenant/{tenantId}/automation/{automationId}
Expand All @@ -19,7 +23,16 @@ export default async (req, res) => {
new PermissionChecker(req).validateHas(Permissions.values.automationDestroy)
await new AutomationService(req).destroy(req.params.automationId)

await req.posthog.reloadFeatureFlags()

track('Automation Destroyed', { id: req.params.automationId }, { ...req })
identifyTenant(req)

const automationCount = await AutomationRepository.countAll(req.database, req.currentTenant.id)
await ensureFlagUpdated(FeatureFlag.AUTOMATIONS, req.currentTenant.id, req.posthog, {
plan: req.currentTenant.plan,
automationCount,
})

await req.responseHandler.success(req, res, true, 204)
}
8 changes: 7 additions & 1 deletion backend/src/api/automation/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { safeWrap } from '../../middlewares/errorMiddleware'
import { featureFlagMiddleware } from '../../middlewares/featureFlagMiddleware'
import { FeatureFlag } from '../../types/common'

export default (app) => {
app.post('/tenant/:tenantId/automation', safeWrap(require('./automationCreate').default))
app.post(
'/tenant/:tenantId/automation',
featureFlagMiddleware(FeatureFlag.AUTOMATIONS, 'entities.automation.errors.planLimitExceeded'),
safeWrap(require('./automationCreate').default),
)
app.put(
'/tenant/:tenantId/automation/:automationId',
safeWrap(require('./automationUpdate').default),
Expand Down
23 changes: 23 additions & 0 deletions backend/src/api/conversation/conversationSettingsUpdate.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,33 @@
import Error403 from '../../errors/Error403'
import isFeatureEnabled from '../../feature-flags/isFeatureEnabled'
import Permissions from '../../security/permissions'
import ConversationService from '../../services/conversationService'
import PermissionChecker from '../../services/user/permissionChecker'
import { FeatureFlag } from '../../types/common'

export default async (req, res) => {
new PermissionChecker(req).validateHas(Permissions.values.conversationEdit)

if (
req.body.customUrl &&
!(await isFeatureEnabled(
FeatureFlag.COMMUNITY_HELP_CENTER_PRO,
req.currentTenant.id,
req.posthog,
))
) {
await req.responseHandler.error(
req,
res,
new Error403(
req.language,
'communityHelpCenter.errors.planNotSupportingCustomUrls',
req.currentTenant.plan,
),
)
return
}

const payload = await new ConversationService(req).updateSettings(req.body)

await req.responseHandler.success(req, res, payload)
Expand Down
14 changes: 14 additions & 0 deletions backend/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import express from 'express'
import cors from 'cors'
import helmet from 'helmet'
import bunyanMiddleware from 'bunyan-middleware'
import { PostHog } from 'posthog-node'
import { authMiddleware } from '../middlewares/authMiddleware'
import { tenantMiddleware } from '../middlewares/tenantMiddleware'
import { databaseMiddleware } from '../middlewares/databaseMiddleware'
Expand All @@ -16,6 +17,7 @@ import { errorMiddleware } from '../middlewares/errorMiddleware'
import { passportStrategyMiddleware } from '../middlewares/passportStrategyMiddleware'
import { redisMiddleware } from '../middlewares/redisMiddleware'
import { createRedisClient } from '../utils/redis'
import { POSTHOG_CONFIG } from '../config'

const serviceLogger = createServiceLogger()

Expand All @@ -24,6 +26,12 @@ const app = express()
setImmediate(async () => {
const redis = await createRedisClient(true)

let posthog = null

if (POSTHOG_CONFIG.apiKey) {
posthog = new PostHog(POSTHOG_CONFIG.apiKey)
}

// Enables CORS
app.use(cors({ origin: true }))

Expand All @@ -47,6 +55,12 @@ setImmediate(async () => {
// Bind redis to request
app.use(redisMiddleware(redis))

// Bind posthog to request
app.use((req: any, res, next) => {
req.posthog = posthog
next()
})

// initialize passport strategies
app.use(passportStrategyMiddleware)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('Integration protected fields tests', () => {

describe('Integration create protected fields', () => {
it('Should create an integratio without protected variables', async () => {
const plan = Plans.values.free
const plan = Plans.values.essential
const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db, plan)
const tenantId = mockIServiceOptions.currentTenant.get({ plain: true }).id
const token = SequelizeTestUtils.getUserToken(mockIServiceOptions)
Expand All @@ -37,7 +37,7 @@ describe('Integration protected fields tests', () => {
})
})
it('Should throw an error when limitCount is passed for a free plan', async () => {
const plan = Plans.values.free
const plan = Plans.values.essential
const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db, plan)
const tenantId = mockIServiceOptions.currentTenant.get({ plain: true }).id
const token = SequelizeTestUtils.getUserToken(mockIServiceOptions)
Expand All @@ -59,7 +59,7 @@ describe('Integration protected fields tests', () => {

describe('Integration update protected fields', () => {
it('Should update an integratiom without protected variables', async () => {
const plan = Plans.values.free
const plan = Plans.values.essential
const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db, plan)
const integrationId = (
await new IntegrationService(mockIServiceOptions).create({
Expand All @@ -83,7 +83,7 @@ describe('Integration protected fields tests', () => {
})
})
it('Should throw an error when limitCount is passed', async () => {
const plan = Plans.values.free
const plan = Plans.values.essential
const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db, plan)
const integrationId = (
await new IntegrationService(mockIServiceOptions).create({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('Microservice protected fields tests', () => {

describe('Microservice create protected fields', () => {
it('Should return 200 with free variant', async () => {
const plan = Plans.values.free
const plan = Plans.values.essential
const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db, plan)
const tenantId = mockIServiceOptions.currentTenant.get({ plain: true }).id
const token = SequelizeTestUtils.getUserToken(mockIServiceOptions)
Expand All @@ -38,7 +38,7 @@ describe('Microservice protected fields tests', () => {
})

it('Should return 403 with a premium microservice in a free tenant', async () => {
const plan = Plans.values.free
const plan = Plans.values.essential
const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db, plan)
const tenantId = mockIServiceOptions.currentTenant.get({ plain: true }).id
const token = SequelizeTestUtils.getUserToken(mockIServiceOptions)
Expand All @@ -58,27 +58,7 @@ describe('Microservice protected fields tests', () => {
})

it('Should return 200 with a premium microservice in a premium tenant', async () => {
const plan = Plans.values.premium
const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db, plan)
const tenantId = mockIServiceOptions.currentTenant.get({ plain: true }).id
const token = SequelizeTestUtils.getUserToken(mockIServiceOptions)
const data = {
type: 'check_merge',
variant: 'premium',
}
return request(app)
.post(`/tenant/${tenantId}/microservice`)
.set({
Authorization: `Bearer ${token}`,
})
.send({ ...data })
.then((response) => {
expect(response.statusCode).toBe(200)
})
})

it('Should return 200 with a premium microservice in a beta tenant', async () => {
const plan = Plans.values.beta
const plan = Plans.values.growth
const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db, plan)
const tenantId = mockIServiceOptions.currentTenant.get({ plain: true }).id
const token = SequelizeTestUtils.getUserToken(mockIServiceOptions)
Expand All @@ -100,7 +80,7 @@ describe('Microservice protected fields tests', () => {

describe('Microservice update protected fields', () => {
it('Should return 200 without protected variables', async () => {
const plan = Plans.values.free
const plan = Plans.values.essential
const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db, plan)
const microServiceId = (
await new MicroserviceService(mockIServiceOptions).create({
Expand All @@ -126,7 +106,7 @@ describe('Microservice protected fields tests', () => {
})

it('Should return 200 when updating with default variant', async () => {
const plan = Plans.values.free
const plan = Plans.values.essential
const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db, plan)
const microServiceId = (
await new MicroserviceService(mockIServiceOptions).create({
Expand All @@ -153,7 +133,7 @@ describe('Microservice protected fields tests', () => {
})

it('Should return 403 when updating with to premium variant in free tenant', async () => {
const plan = Plans.values.free
const plan = Plans.values.essential
const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db, plan)
const microServiceId = (
await new MicroserviceService(mockIServiceOptions).create({
Expand All @@ -179,7 +159,7 @@ describe('Microservice protected fields tests', () => {
})

it('Should return 200 when updating with premium variant in premium tenant', async () => {
const plan = Plans.values.premium
const plan = Plans.values.growth
const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db, plan)
const microServiceId = (
await new MicroserviceService(mockIServiceOptions).create({
Expand All @@ -205,34 +185,8 @@ describe('Microservice protected fields tests', () => {
})
})

it('Should return 200 when updating with premium variant in beta tenant', async () => {
const plan = Plans.values.beta
const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db, plan)
const microServiceId = (
await new MicroserviceService(mockIServiceOptions).create({
type: 'check_merge',
running: false,
variant: 'default',
})
).id
const tenantId = mockIServiceOptions.currentTenant.get({ plain: true }).id
const token = SequelizeTestUtils.getUserToken(mockIServiceOptions)
const data = {
running: true,
variant: 'premium',
}
return request(app)
.put(`/tenant/${tenantId}/microservice/${microServiceId}`)
.set({
Authorization: `Bearer ${token}`,
})
.send({ ...data })
.then((response) => {
expect(response.statusCode).toBe(200)
})
})
it('Should return 200 when updating from premium to default in premium tenant', async () => {
const plan = Plans.values.premium
const plan = Plans.values.growth
const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db, plan)
const microServiceId = (
await new MicroserviceService(mockIServiceOptions).create({
Expand Down
2 changes: 1 addition & 1 deletion backend/src/api/plan/stripe/checkout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default async (req, res) => {
}

if (
currentTenant.plan !== Plans.values.free &&
currentTenant.plan !== Plans.values.essential &&
currentTenant.planStatus !== 'cancel_at_period_end' &&
currentTenant.planUserId !== currentUser.id
) {
Expand Down
Loading