From 78a7ae9ec19a0b23a5dd48321d1ff452a9789cfc Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 3 Jan 2025 14:59:57 +0000 Subject: [PATCH 1/4] Convert portal licensing store to TS --- .../builder/src/stores/portal/licensing.js | 279 ---------------- .../builder/src/stores/portal/licensing.ts | 309 ++++++++++++++++++ packages/types/src/api/web/global/self.ts | 8 +- 3 files changed, 316 insertions(+), 280 deletions(-) delete mode 100644 packages/builder/src/stores/portal/licensing.js create mode 100644 packages/builder/src/stores/portal/licensing.ts diff --git a/packages/builder/src/stores/portal/licensing.js b/packages/builder/src/stores/portal/licensing.js deleted file mode 100644 index afc3ea1628..0000000000 --- a/packages/builder/src/stores/portal/licensing.js +++ /dev/null @@ -1,279 +0,0 @@ -import { writable, get } from "svelte/store" -import { API } from "@/api" -import { auth, admin } from "@/stores/portal" -import { Constants } from "@budibase/frontend-core" -import { StripeStatus } from "@/components/portal/licensing/constants" -import { PlanModel } from "@budibase/types" - -const UNLIMITED = -1 - -export const createLicensingStore = () => { - const DEFAULT = { - // navigation - goToUpgradePage: () => {}, - goToPricingPage: () => {}, - // the top level license - license: undefined, - isFreePlan: true, - isEnterprisePlan: true, - isBusinessPlan: true, - // features - groupsEnabled: false, - backupsEnabled: false, - brandingEnabled: false, - scimEnabled: false, - environmentVariablesEnabled: false, - budibaseAIEnabled: false, - customAIConfigsEnabled: false, - auditLogsEnabled: false, - // the currently used quotas from the db - quotaUsage: undefined, - // derived quota metrics for percentages used - usageMetrics: undefined, - // quota reset - quotaResetDaysRemaining: undefined, - quotaResetDate: undefined, - // failed payments - accountPastDue: undefined, - pastDueEndDate: undefined, - pastDueDaysRemaining: undefined, - accountDowngraded: undefined, - // user limits - userCount: undefined, - userLimit: undefined, - userLimitReached: false, - errUserLimit: false, - } - - const oneDayInMilliseconds = 86400000 - - const store = writable(DEFAULT) - - function usersLimitReached(userCount, userLimit) { - if (userLimit === UNLIMITED) { - return false - } - return userCount >= userLimit - } - - function usersLimitExceeded(userCount, userLimit) { - if (userLimit === UNLIMITED) { - return false - } - return userCount > userLimit - } - - async function isCloud() { - let adminStore = get(admin) - if (!adminStore.loaded) { - await admin.init() - adminStore = get(admin) - } - return adminStore.cloud - } - - const actions = { - init: async () => { - actions.setNavigation() - actions.setLicense() - await actions.setQuotaUsage() - }, - setNavigation: () => { - const adminStore = get(admin) - const authStore = get(auth) - - const upgradeUrl = authStore?.user?.accountPortalAccess - ? `${adminStore.accountPortalUrl}/portal/upgrade` - : "/builder/portal/account/upgrade" - - const goToUpgradePage = () => { - window.location.href = upgradeUrl - } - const goToPricingPage = () => { - window.open("https://budibase.com/pricing/", "_blank") - } - store.update(state => { - return { - ...state, - goToUpgradePage, - goToPricingPage, - } - }) - }, - setLicense: () => { - const license = get(auth).user.license - const planType = license?.plan.type - const isEnterprisePlan = planType === Constants.PlanType.ENTERPRISE - const isFreePlan = planType === Constants.PlanType.FREE - const isBusinessPlan = planType === Constants.PlanType.BUSINESS - const isEnterpriseTrial = - planType === Constants.PlanType.ENTERPRISE_BASIC_TRIAL - const groupsEnabled = license.features.includes( - Constants.Features.USER_GROUPS - ) - const backupsEnabled = license.features.includes( - Constants.Features.APP_BACKUPS - ) - const scimEnabled = license.features.includes(Constants.Features.SCIM) - const environmentVariablesEnabled = license.features.includes( - Constants.Features.ENVIRONMENT_VARIABLES - ) - const enforceableSSO = license.features.includes( - Constants.Features.ENFORCEABLE_SSO - ) - const brandingEnabled = license.features.includes( - Constants.Features.BRANDING - ) - const auditLogsEnabled = license.features.includes( - Constants.Features.AUDIT_LOGS - ) - const syncAutomationsEnabled = license.features.includes( - Constants.Features.SYNC_AUTOMATIONS - ) - const triggerAutomationRunEnabled = license.features.includes( - Constants.Features.TRIGGER_AUTOMATION_RUN - ) - const perAppBuildersEnabled = license.features.includes( - Constants.Features.APP_BUILDERS - ) - const budibaseAIEnabled = license.features.includes( - Constants.Features.BUDIBASE_AI - ) - const customAIConfigsEnabled = license.features.includes( - Constants.Features.AI_CUSTOM_CONFIGS - ) - store.update(state => { - return { - ...state, - license, - isEnterprisePlan, - isFreePlan, - isBusinessPlan, - isEnterpriseTrial, - groupsEnabled, - backupsEnabled, - brandingEnabled, - budibaseAIEnabled, - customAIConfigsEnabled, - scimEnabled, - environmentVariablesEnabled, - auditLogsEnabled, - enforceableSSO, - syncAutomationsEnabled, - triggerAutomationRunEnabled, - perAppBuildersEnabled, - } - }) - }, - setQuotaUsage: async () => { - const quotaUsage = await API.getQuotaUsage() - store.update(state => { - return { - ...state, - quotaUsage, - } - }) - await actions.setUsageMetrics() - }, - usersLimitReached: userCount => { - return usersLimitReached(userCount, get(store).userLimit) - }, - usersLimitExceeded(userCount) { - return usersLimitExceeded(userCount, get(store).userLimit) - }, - setUsageMetrics: async () => { - const usage = get(store).quotaUsage - const license = get(auth).user.license - const now = new Date() - - const getMetrics = (keys, license, quota) => { - if (!license || !quota || !keys) { - return {} - } - return keys.reduce((acc, key) => { - const quotaLimit = license[key].value - const quotaUsed = (quota[key] / quotaLimit) * 100 - acc[key] = quotaLimit > -1 ? Math.floor(quotaUsed) : -1 - return acc - }, {}) - } - const monthlyMetrics = getMetrics( - ["queries", "automations"], - license.quotas.usage.monthly, - usage.monthly.current - ) - const staticMetrics = getMetrics( - ["apps", "rows"], - license.quotas.usage.static, - usage.usageQuota - ) - - const getDaysBetween = (dateStart, dateEnd) => { - return dateEnd > dateStart - ? Math.round( - (dateEnd.getTime() - dateStart.getTime()) / oneDayInMilliseconds - ) - : 0 - } - - const quotaResetDate = new Date(usage.quotaReset) - const quotaResetDaysRemaining = getDaysBetween(now, quotaResetDate) - - const accountDowngraded = - license?.billing?.subscription?.downgradeAt && - license?.billing?.subscription?.downgradeAt <= now.getTime() && - license?.billing?.subscription?.status === StripeStatus.PAST_DUE && - license?.plan.type === Constants.PlanType.FREE - - const pastDueAtMilliseconds = license?.billing?.subscription?.pastDueAt - const downgradeAtMilliseconds = - license?.billing?.subscription?.downgradeAt - let pastDueDaysRemaining - let pastDueEndDate - - if (pastDueAtMilliseconds && downgradeAtMilliseconds) { - pastDueEndDate = new Date(downgradeAtMilliseconds) - pastDueDaysRemaining = getDaysBetween( - new Date(pastDueAtMilliseconds), - pastDueEndDate - ) - } - - const userQuota = license.quotas.usage.static.users - const userLimit = userQuota?.value - const userCount = usage.usageQuota.users - const userLimitReached = usersLimitReached(userCount, userLimit) - const userLimitExceeded = usersLimitExceeded(userCount, userLimit) - const isCloudAccount = await isCloud() - const errUserLimit = - isCloudAccount && - license.plan.model === PlanModel.PER_USER && - userLimitExceeded - - store.update(state => { - return { - ...state, - usageMetrics: { ...monthlyMetrics, ...staticMetrics }, - quotaResetDaysRemaining, - quotaResetDate, - accountDowngraded, - accountPastDue: pastDueAtMilliseconds != null, - pastDueEndDate, - pastDueDaysRemaining, - // user limits - userCount, - userLimit, - userLimitReached, - errUserLimit, - } - }) - }, - } - - return { - subscribe: store.subscribe, - ...actions, - } -} - -export const licensing = createLicensingStore() diff --git a/packages/builder/src/stores/portal/licensing.ts b/packages/builder/src/stores/portal/licensing.ts new file mode 100644 index 0000000000..21250caeca --- /dev/null +++ b/packages/builder/src/stores/portal/licensing.ts @@ -0,0 +1,309 @@ +import { get } from "svelte/store" +import { API } from "@/api" +import { auth, admin } from "@/stores/portal" +import { Constants } from "@budibase/frontend-core" +import { StripeStatus } from "@/components/portal/licensing/constants" +import { + MonthlyQuotaName, + PlanModel, + QuotaUsage, + StaticQuotaName, +} from "@budibase/types" +import { BudiStore } from "../BudiStore" + +const UNLIMITED = -1 +const ONE_DAY_MILLIS = 86400000 + +type MonthlyMetrics = { [key in MonthlyQuotaName]?: number } +type StaticMetrics = { [key in StaticQuotaName]?: number } + +interface LicensingState { + goToUpgradePage: () => void + goToPricingPage: () => void + // the top level license + license: any + isFreePlan: boolean + isEnterprisePlan: boolean + isBusinessPlan: boolean + // features + groupsEnabled: boolean + backupsEnabled: boolean + brandingEnabled: boolean + scimEnabled: boolean + environmentVariablesEnabled: boolean + budibaseAIEnabled: boolean + customAIConfigsEnabled: boolean + auditLogsEnabled: boolean + // the currently used quotas from the db + quotaUsage?: QuotaUsage + // derived quota metrics for percentages used + usageMetrics: any + // quota reset + quotaResetDaysRemaining: any + quotaResetDate: any + // failed payments + accountPastDue: any + pastDueEndDate: any + pastDueDaysRemaining: any + accountDowngraded: any + // user limits + userCount: any + userLimit: any + userLimitReached: boolean + errUserLimit: boolean +} + +class LicensingStore extends BudiStore { + constructor() { + super({ + // navigation + goToUpgradePage: () => {}, + goToPricingPage: () => {}, + // the top level license + license: undefined, + isFreePlan: true, + isEnterprisePlan: true, + isBusinessPlan: true, + // features + groupsEnabled: false, + backupsEnabled: false, + brandingEnabled: false, + scimEnabled: false, + environmentVariablesEnabled: false, + budibaseAIEnabled: false, + customAIConfigsEnabled: false, + auditLogsEnabled: false, + // the currently used quotas from the db + quotaUsage: undefined, + // derived quota metrics for percentages used + usageMetrics: undefined, + // quota reset + quotaResetDaysRemaining: undefined, + quotaResetDate: undefined, + // failed payments + accountPastDue: undefined, + pastDueEndDate: undefined, + pastDueDaysRemaining: undefined, + accountDowngraded: undefined, + // user limits + userCount: undefined, + userLimit: undefined, + userLimitReached: false, + errUserLimit: false, + }) + } + + usersLimitReached( + userCount: number, + userLimit: number = get(this.store).userLimit + ) { + if (userLimit === UNLIMITED) { + return false + } + return userCount >= userLimit + } + + usersLimitExceeded( + userCount: number, + userLimit: number = get(this.store).userLimit + ) { + if (userLimit === UNLIMITED) { + return false + } + return userCount > userLimit + } + + async isCloud() { + let adminStore = get(admin) + if (!adminStore.loaded) { + await admin.init() + adminStore = get(admin) + } + return adminStore.cloud + } + + async init() { + this.setNavigation() + this.setLicense() + await this.setQuotaUsage() + } + + setNavigation() { + const adminStore = get(admin) + const authStore = get(auth) + + const upgradeUrl = authStore?.user?.accountPortalAccess + ? `${adminStore.accountPortalUrl}/portal/upgrade` + : "/builder/portal/account/upgrade" + + const goToUpgradePage = () => { + window.location.href = upgradeUrl + } + const goToPricingPage = () => { + window.open("https://budibase.com/pricing/", "_blank") + } + this.update(state => { + return { + ...state, + goToUpgradePage, + goToPricingPage, + } + }) + } + + setLicense() { + const license = get(auth).user?.license + const planType = license?.plan.type + const features = license?.features || [] + const isEnterprisePlan = planType === Constants.PlanType.ENTERPRISE + const isFreePlan = planType === Constants.PlanType.FREE + const isBusinessPlan = planType === Constants.PlanType.BUSINESS + const isEnterpriseTrial = + planType === Constants.PlanType.ENTERPRISE_BASIC_TRIAL + const groupsEnabled = features.includes(Constants.Features.USER_GROUPS) + const backupsEnabled = features.includes(Constants.Features.APP_BACKUPS) + const scimEnabled = features.includes(Constants.Features.SCIM) + const environmentVariablesEnabled = features.includes( + Constants.Features.ENVIRONMENT_VARIABLES + ) + const enforceableSSO = features.includes(Constants.Features.ENFORCEABLE_SSO) + const brandingEnabled = features.includes(Constants.Features.BRANDING) + const auditLogsEnabled = features.includes(Constants.Features.AUDIT_LOGS) + const syncAutomationsEnabled = features.includes( + Constants.Features.SYNC_AUTOMATIONS + ) + const triggerAutomationRunEnabled = features.includes( + Constants.Features.TRIGGER_AUTOMATION_RUN + ) + const perAppBuildersEnabled = features.includes( + Constants.Features.APP_BUILDERS + ) + const budibaseAIEnabled = features.includes(Constants.Features.BUDIBASE_AI) + const customAIConfigsEnabled = features.includes( + Constants.Features.AI_CUSTOM_CONFIGS + ) + this.update(state => { + return { + ...state, + license, + isEnterprisePlan, + isFreePlan, + isBusinessPlan, + isEnterpriseTrial, + groupsEnabled, + backupsEnabled, + brandingEnabled, + budibaseAIEnabled, + customAIConfigsEnabled, + scimEnabled, + environmentVariablesEnabled, + auditLogsEnabled, + enforceableSSO, + syncAutomationsEnabled, + triggerAutomationRunEnabled, + perAppBuildersEnabled, + } + }) + } + + async setQuotaUsage() { + const quotaUsage = await API.getQuotaUsage() + this.update(state => { + return { + ...state, + quotaUsage, + } + }) + await this.setUsageMetrics() + } + + async setUsageMetrics() { + const usage = get(this.store).quotaUsage + const license = get(auth).user?.license + const now = new Date() + if (!license || !usage) { + return + } + + // Process monthly metrics + const monthlyMetrics = [ + MonthlyQuotaName.QUERIES, + MonthlyQuotaName.AUTOMATIONS, + ].reduce((acc: MonthlyMetrics, key) => { + const limit = license.quotas.usage.monthly[key].value + const used = (usage.monthly.current?.[key] || 0 / limit) * 100 + acc[key] = limit > -1 ? Math.floor(used) : -1 + return acc + }, {}) + + // Process static metrics + const staticMetrics = [StaticQuotaName.APPS, StaticQuotaName.ROWS].reduce( + (acc: StaticMetrics, key) => { + const limit = license.quotas.usage.static[key].value + const used = (usage.usageQuota[key] || 0 / limit) * 100 + acc[key] = limit > -1 ? Math.floor(used) : -1 + return acc + }, + {} + ) + + const getDaysBetween = (dateStart: Date, dateEnd: Date) => { + return dateEnd > dateStart + ? Math.round((dateEnd.getTime() - dateStart.getTime()) / ONE_DAY_MILLIS) + : 0 + } + + const quotaResetDate = new Date(usage.quotaReset) + const quotaResetDaysRemaining = getDaysBetween(now, quotaResetDate) + + const accountDowngraded = + license.billing?.subscription?.downgradeAt && + license.billing?.subscription?.downgradeAt <= now.getTime() && + license.billing?.subscription?.status === StripeStatus.PAST_DUE && + license.plan.type === Constants.PlanType.FREE + + const pastDueAtMilliseconds = license.billing?.subscription?.pastDueAt + const downgradeAtMilliseconds = license.billing?.subscription?.downgradeAt + let pastDueDaysRemaining: number + let pastDueEndDate: Date + + if (pastDueAtMilliseconds && downgradeAtMilliseconds) { + pastDueEndDate = new Date(downgradeAtMilliseconds) + pastDueDaysRemaining = getDaysBetween( + new Date(pastDueAtMilliseconds), + pastDueEndDate + ) + } + + const userQuota = license.quotas.usage.static.users + const userLimit = userQuota.value + const userCount = usage.usageQuota.users + const userLimitReached = this.usersLimitReached(userCount, userLimit) + const userLimitExceeded = this.usersLimitExceeded(userCount, userLimit) + const isCloudAccount = await this.isCloud() + const errUserLimit = + isCloudAccount && + license.plan.model === PlanModel.PER_USER && + userLimitExceeded + + this.update(state => { + return { + ...state, + usageMetrics: { ...monthlyMetrics, ...staticMetrics }, + quotaResetDaysRemaining, + quotaResetDate, + accountDowngraded, + accountPastDue: pastDueAtMilliseconds != null, + pastDueEndDate, + pastDueDaysRemaining, + // user limits + userCount, + userLimit, + userLimitReached, + errUserLimit, + } + }) + } +} + +export const licensing = new LicensingStore() diff --git a/packages/types/src/api/web/global/self.ts b/packages/types/src/api/web/global/self.ts index c478e89dd6..085c8a60f6 100644 --- a/packages/types/src/api/web/global/self.ts +++ b/packages/types/src/api/web/global/self.ts @@ -1,4 +1,5 @@ -import { DevInfo, User } from "../../../documents" +import { License } from "../../../sdk" +import { Account, DevInfo, User } from "../../../documents" export interface GenerateAPIKeyRequest { userId?: string @@ -9,4 +10,9 @@ export interface FetchAPIKeyResponse extends DevInfo {} export interface GetGlobalSelfResponse extends User { flags?: Record + account?: Account + license: License + budibaseAccess: boolean + accountPortalAccess: boolean + csrfToken: boolean } From bffdc95305b1b2921d15ca5791648d5af629efc9 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 3 Jan 2025 15:10:39 +0000 Subject: [PATCH 2/4] Add more explicit types --- .../builder/src/stores/portal/licensing.ts | 42 +++++++++---------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/packages/builder/src/stores/portal/licensing.ts b/packages/builder/src/stores/portal/licensing.ts index 21250caeca..0b56e14005 100644 --- a/packages/builder/src/stores/portal/licensing.ts +++ b/packages/builder/src/stores/portal/licensing.ts @@ -4,6 +4,7 @@ import { auth, admin } from "@/stores/portal" import { Constants } from "@budibase/frontend-core" import { StripeStatus } from "@/components/portal/licensing/constants" import { + License, MonthlyQuotaName, PlanModel, QuotaUsage, @@ -16,12 +17,13 @@ const ONE_DAY_MILLIS = 86400000 type MonthlyMetrics = { [key in MonthlyQuotaName]?: number } type StaticMetrics = { [key in StaticQuotaName]?: number } +type UsageMetrics = MonthlyMetrics & StaticMetrics interface LicensingState { goToUpgradePage: () => void goToPricingPage: () => void // the top level license - license: any + license?: License isFreePlan: boolean isEnterprisePlan: boolean isBusinessPlan: boolean @@ -37,18 +39,18 @@ interface LicensingState { // the currently used quotas from the db quotaUsage?: QuotaUsage // derived quota metrics for percentages used - usageMetrics: any + usageMetrics?: UsageMetrics // quota reset - quotaResetDaysRemaining: any - quotaResetDate: any + quotaResetDaysRemaining?: number + quotaResetDate?: Date // failed payments - accountPastDue: any - pastDueEndDate: any - pastDueDaysRemaining: any - accountDowngraded: any + accountPastDue: boolean + pastDueEndDate?: Date + pastDueDaysRemaining?: number + accountDowngraded: boolean // user limits - userCount: any - userLimit: any + userCount?: number + userLimit?: number userLimitReached: boolean errUserLimit: boolean } @@ -81,10 +83,10 @@ class LicensingStore extends BudiStore { quotaResetDaysRemaining: undefined, quotaResetDate: undefined, // failed payments - accountPastDue: undefined, + accountPastDue: false, pastDueEndDate: undefined, pastDueDaysRemaining: undefined, - accountDowngraded: undefined, + accountDowngraded: false, // user limits userCount: undefined, userLimit: undefined, @@ -93,21 +95,15 @@ class LicensingStore extends BudiStore { }) } - usersLimitReached( - userCount: number, - userLimit: number = get(this.store).userLimit - ) { - if (userLimit === UNLIMITED) { + usersLimitReached(userCount: number, userLimit = get(this.store).userLimit) { + if (userLimit === UNLIMITED || userLimit === undefined) { return false } return userCount >= userLimit } - usersLimitExceeded( - userCount: number, - userLimit: number = get(this.store).userLimit - ) { - if (userLimit === UNLIMITED) { + usersLimitExceeded(userCount: number, userLimit = get(this.store).userLimit) { + if (userLimit === UNLIMITED || userLimit === undefined) { return false } return userCount > userLimit @@ -257,7 +253,7 @@ class LicensingStore extends BudiStore { const quotaResetDaysRemaining = getDaysBetween(now, quotaResetDate) const accountDowngraded = - license.billing?.subscription?.downgradeAt && + !!license.billing?.subscription?.downgradeAt && license.billing?.subscription?.downgradeAt <= now.getTime() && license.billing?.subscription?.status === StripeStatus.PAST_DUE && license.plan.type === Constants.PlanType.FREE From 97970043f1f35cd82a38a2e924fae10fc641f76d Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 3 Jan 2025 15:16:12 +0000 Subject: [PATCH 3/4] Fix global self response type --- .../worker/src/api/controllers/global/self.ts | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/worker/src/api/controllers/global/self.ts b/packages/worker/src/api/controllers/global/self.ts index f8488f526b..3464bff88f 100644 --- a/packages/worker/src/api/controllers/global/self.ts +++ b/packages/worker/src/api/controllers/global/self.ts @@ -84,15 +84,15 @@ export async function fetchAPIKey(ctx: UserCtx) { } /** - * Add the attributes that are session based to the current user. + * */ -const addSessionAttributesToUser = (ctx: any) => { - ctx.body.account = ctx.user.account - ctx.body.license = ctx.user.license - ctx.body.budibaseAccess = !!ctx.user.budibaseAccess - ctx.body.accountPortalAccess = !!ctx.user.accountPortalAccess - ctx.body.csrfToken = ctx.user.csrfToken -} +const getUserSessionAttributes = (ctx: any) => ({ + account: ctx.user.account, + license: ctx.user.license, + budibaseAccess: !!ctx.user.budibaseAccess, + accountPortalAccess: !!ctx.user.accountPortalAccess, + csrfToken: ctx.user.csrfToken, +}) export async function getSelf(ctx: UserCtx) { if (!ctx.user) { @@ -108,13 +108,19 @@ export async function getSelf(ctx: UserCtx) { // get the main body of the user const user = await userSdk.db.getUser(userId) - ctx.body = await groups.enrichUserRolesFromGroups(user) + const enrichedUser = await groups.enrichUserRolesFromGroups(user) + + // add the attributes that are session based to the current user + const sessionAttributes = getUserSessionAttributes(ctx) // add the feature flags for this tenant const flags = await features.flags.fetch() - ctx.body.flags = flags - addSessionAttributesToUser(ctx) + ctx.body = { + ...enrichedUser, + ...sessionAttributes, + flags, + } } export const syncAppFavourites = async (processedAppIds: string[]) => { From 19f39af9ec4cc0cee2667f7a3c0eb4b29f83f0fd Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 7 Jan 2025 14:04:51 +0000 Subject: [PATCH 4/4] Fix conflict --- packages/worker/src/api/controllers/global/self.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/worker/src/api/controllers/global/self.ts b/packages/worker/src/api/controllers/global/self.ts index c8d99f332c..3464bff88f 100644 --- a/packages/worker/src/api/controllers/global/self.ts +++ b/packages/worker/src/api/controllers/global/self.ts @@ -114,11 +114,7 @@ export async function getSelf(ctx: UserCtx) { const sessionAttributes = getUserSessionAttributes(ctx) // add the feature flags for this tenant -<<<<<<< HEAD const flags = await features.flags.fetch() -======= - ctx.body.flags = await features.flags.fetch() ->>>>>>> 55685be0814403644bc25d82b3218e27f6590490 ctx.body = { ...enrichedUser,