Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions packages/server/src/IdentityManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,16 @@ export class IdentityManager {
}
}

public static disableForCloud(message: string) {
return (_req: Request, res: Response, next: NextFunction) => {
const identityManager = getRunningExpressApp().identityManager
if (identityManager.isCloud()) {
return res.status(403).json({ message })
}
return next()
}
}

public async createStripeCustomerPortalSession(req: Request) {
if (!this.stripeManager) {
throw new Error('Stripe manager is not initialized')
Expand Down
9 changes: 9 additions & 0 deletions packages/server/src/StripeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import Stripe from 'stripe'
import { UserPlan } from './Interface'
import { UsageCacheManager } from './UsageCacheManager'
import { LICENSE_QUOTAS } from './utils/constants'
import { InternalFlowiseError } from './errors/internalFlowiseError'
import { StatusCodes } from 'http-status-codes'

export class StripeManager {
private static instance: StripeManager
Expand Down Expand Up @@ -53,6 +55,7 @@ export class StripeManager {

try {
const subscription = await this.stripe.subscriptions.retrieve(subscriptionId)
if (subscription.status === 'canceled') throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, 'Subscription is canceled')
const items = subscription.items.data
if (items.length === 0) {
return ''
Expand Down Expand Up @@ -85,6 +88,7 @@ export class StripeManager {
const subscription = await this.stripe.subscriptions.retrieve(subscriptionId, {
timeout: 5000
})
if (subscription.status === 'canceled') throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, 'Subscription is canceled')
const items = subscription.items.data
if (items.length === 0) {
return {}
Expand Down Expand Up @@ -241,6 +245,7 @@ export class StripeManager {

try {
const subscription = await this.stripe.subscriptions.retrieve(subscriptionId)
if (subscription.status === 'canceled') throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, 'Subscription is canceled')
const additionalSeatsItem = subscription.items.data.find(
(item) => (item.price.product as string) === process.env.ADDITIONAL_SEAT_ID
)
Expand Down Expand Up @@ -282,6 +287,7 @@ export class StripeManager {

try {
const subscription = await this.stripe.subscriptions.retrieve(subscriptionId)
if (subscription.status === 'canceled') throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, 'Subscription is canceled')

// Get customer's credit balance
const customer = await this.stripe.customers.retrieve(subscription.customer as string)
Expand Down Expand Up @@ -374,6 +380,7 @@ export class StripeManager {

try {
const subscription = await this.stripe.subscriptions.retrieve(subscriptionId)
if (subscription.status === 'canceled') throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, 'Subscription is canceled')
const additionalSeatsItem = subscription.items.data.find(
(item) => (item.price.product as string) === process.env.ADDITIONAL_SEAT_ID
)
Expand Down Expand Up @@ -438,6 +445,7 @@ export class StripeManager {

try {
const subscription = await this.stripe.subscriptions.retrieve(subscriptionId)
if (subscription.status === 'canceled') throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, 'Subscription is canceled')
const customerId = subscription.customer as string

// Get customer's credit balance and metadata
Expand Down Expand Up @@ -510,6 +518,7 @@ export class StripeManager {

try {
const subscription = await this.stripe.subscriptions.retrieve(subscriptionId)
if (subscription.status === 'canceled') throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, 'Subscription is canceled')
const customerId = subscription.customer as string

// Get customer details and metadata
Expand Down
4 changes: 4 additions & 0 deletions packages/server/src/UsageCacheManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { Cache, createCache } from 'cache-manager'
import { MODE } from './Interface'
import { LICENSE_QUOTAS } from './utils/constants'
import { StripeManager } from './StripeManager'
import { InternalFlowiseError } from './errors/internalFlowiseError'
import { StatusCodes } from 'http-status-codes'

const DISABLED_QUOTAS = {
[LICENSE_QUOTAS.PREDICTIONS_LIMIT]: 0,
Expand Down Expand Up @@ -100,6 +102,7 @@ export class UsageCacheManager {

// If not in cache, retrieve from Stripe
const subscription = await stripeManager.getStripe().subscriptions.retrieve(subscriptionId)
if (subscription.status === 'canceled') throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, 'Subscription is canceled')

// Update subscription data cache
await this.updateSubscriptionDataToCache(subscriptionId, { subsriptionDetails: stripeManager.getSubscriptionObject(subscription) })
Expand All @@ -123,6 +126,7 @@ export class UsageCacheManager {

// If not in cache, retrieve from Stripe
const subscription = await stripeManager.getStripe().subscriptions.retrieve(subscriptionId)
if (subscription.status === 'canceled') throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, 'Subscription is canceled')
const items = subscription.items.data
if (items.length === 0) {
return DISABLED_QUOTAS
Expand Down
13 changes: 11 additions & 2 deletions packages/server/src/enterprise/routes/organization.route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import express from 'express'
import { IdentityManager } from '../../IdentityManager'
import { OrganizationController } from '../controllers/organization.controller'

const router = express.Router()
Expand All @@ -16,11 +17,19 @@ router.get('/customer-default-source', organizationController.getCustomerWithDef

router.get('/additional-seats-proration', organizationController.getAdditionalSeatsProration)

router.post('/update-additional-seats', organizationController.updateAdditionalSeats)
router.post(
'/update-additional-seats',
IdentityManager.disableForCloud('Plan changes are currently disabled.'),
organizationController.updateAdditionalSeats
)

router.get('/plan-proration', organizationController.getPlanProration)

router.post('/update-subscription-plan', organizationController.updateSubscriptionPlan)
router.post(
'/update-subscription-plan',
IdentityManager.disableForCloud('Plan changes are currently disabled.'),
organizationController.updateSubscriptionPlan
)

router.get('/get-current-usage', organizationController.getCurrentUsage)

Expand Down
7 changes: 6 additions & 1 deletion packages/server/src/enterprise/services/account.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,8 @@ export class AccountService {
break
case Platform.CLOUD: {
const user = await this.userService.readUserByEmail(data.user.email, queryRunner)
if (user && (user.status === UserStatus.ACTIVE || user.status === UserStatus.UNVERIFIED))
if (!user) throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, 'New registrations are currently closed.')
if (user.status === UserStatus.ACTIVE || user.status === UserStatus.UNVERIFIED)
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, UserErrorMessage.USER_EMAIL_ALREADY_EXISTS)

if (!data.user.email) throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, UserErrorMessage.INVALID_USER_EMAIL)
Expand Down Expand Up @@ -364,6 +365,8 @@ export class AccountService {
data.role = role
const user = await this.userService.readUserByEmail(data.user.email, queryRunner)
if (!user) {
if (this.identityManager.isCloud())
throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, 'New registrations are currently closed.')
await checkUsageLimit('users', subscriptionId, getRunningExpressApp().usageCacheManager, totalOrgUsers + 1)

// generate a temporary token
Expand Down Expand Up @@ -413,6 +416,8 @@ export class AccountService {

return data
}
if (user.status !== UserStatus.ACTIVE && this.identityManager.isCloud())
throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, 'New registrations are currently closed.')
const { organizationUser } = await this.organizationUserService.readOrganizationUserByOrganizationIdUserId(
data.workspace.organizationId,
user.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { sanitizeUser } from '../../utils/sanitize.util'
import { OrganizationUser, OrganizationUserStatus } from '../database/entities/organization-user.entity'
import { Organization } from '../database/entities/organization.entity'
import { GeneralRole } from '../database/entities/role.entity'
import { User } from '../database/entities/user.entity'
import { User, UserStatus } from '../database/entities/user.entity'
import { WorkspaceUser } from '../database/entities/workspace-user.entity'
import { Workspace } from '../database/entities/workspace.entity'
import { OrganizationErrorMessage, OrganizationService } from './organization.service'
Expand Down Expand Up @@ -215,6 +215,11 @@ export class OrganizationUserService {
const queryRunner = this.dataSource.createQueryRunner()
await queryRunner.connect()

const user = await this.userService.readUserById(data.userId, queryRunner)
if (!user) throw new InternalFlowiseError(StatusCodes.NOT_FOUND, UserErrorMessage.USER_NOT_FOUND)
if (user.status !== UserStatus.ACTIVE && getRunningExpressApp().identityManager.isCloud())
throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, 'New registrations are currently closed.')

const { organization, organizationUser } = await this.readOrganizationUserByOrganizationIdUserId(
data.organizationId,
data.userId,
Expand Down
2 changes: 2 additions & 0 deletions packages/server/src/enterprise/services/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ export class UserService {

public async createNewUser(data: Partial<User>, queryRunner: QueryRunner) {
const user = await this.readUserByEmail(data.email, queryRunner)
if (!user && getRunningExpressApp().identityManager.isCloud())
throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, 'New registrations are currently closed.')
if (user) throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, UserErrorMessage.USER_EMAIL_ALREADY_EXISTS)
if (data.credential) data.credential = this.encryptUserCredential(data.credential)
if (!data.name) data.name = data.email
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { OrganizationErrorMessage, OrganizationService } from './organization.se
import { RoleErrorMessage, RoleService } from './role.service'
import { UserErrorMessage, UserService } from './user.service'
import { WorkspaceErrorMessage, WorkspaceService } from './workspace.service'
import { UserStatus } from '../database/entities/user.entity'

export const enum WorkspaceUserErrorMessage {
INVALID_WORKSPACE_USER_SATUS = 'Invalid Workspace User Status',
Expand Down Expand Up @@ -226,6 +227,11 @@ export class WorkspaceUserService {
const queryRunner = this.dataSource.createQueryRunner()
await queryRunner.connect()

const user = await this.userService.readUserById(data.userId, queryRunner)
if (!user) throw new InternalFlowiseError(StatusCodes.NOT_FOUND, UserErrorMessage.USER_NOT_FOUND)
if (user.status !== UserStatus.ACTIVE && getRunningExpressApp().identityManager.isCloud())
throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, 'New registrations are currently closed.')

const { workspace, workspaceUser } = await this.readWorkspaceUserByWorkspaceIdUserId(data.workspaceId, data.userId, queryRunner)
if (workspace.organizationId !== activeOrganizationId)
throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, WorkspaceErrorMessage.WORKSPACE_NOT_FOUND)
Expand Down
20 changes: 4 additions & 16 deletions packages/server/src/enterprise/sso/SSOBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,23 +61,11 @@ abstract class SSOBase {
if (getRunningExpressApp().identityManager.getPlatformType() === Platform.ENTERPRISE) {
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, UserErrorMessage.USER_NOT_FOUND)
}
// no user found, register the user
const data: any = {
user: {
email: email,
name: profile.displayName || email,
status: UserStatus.ACTIVE,
credential: undefined
}
}
if (getRunningExpressApp().identityManager.getPlatformType() === Platform.CLOUD) {
const accountService = new AccountService()
const newAccount = await accountService.register(data)
wu = newAccount.workspaceUser
wu.workspace = newAccount.workspace
user = newAccount.user
}
if (getRunningExpressApp().identityManager.getPlatformType() === Platform.CLOUD)
throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, 'New registrations are currently closed.')
} else {
if (user.status !== UserStatus.ACTIVE && getRunningExpressApp().identityManager.getPlatformType() === Platform.CLOUD)
throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, 'New registrations are currently closed.')
if (user.status === UserStatus.INVITED) {
const data: any = {
user: {
Expand Down
35 changes: 35 additions & 0 deletions packages/ui/src/layout/MainLayout/AnnouncementBanner.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import PropTypes from 'prop-types'
import { Alert, Link } from '@mui/material'

const AnnouncementBanner = ({ onClose }) => (
<Alert
severity='info'
onClose={onClose}
sx={{
position: 'relative',
borderRadius: 0,
py: 0.5,
'& .MuiAlert-icon': { display: 'none' },
'& .MuiAlert-message': {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexWrap: 'wrap',
gap: 0.5,
width: '100%'
},
'& .MuiAlert-action': { position: 'absolute', right: 8, top: '50%', transform: 'translateY(-50%)', p: 0 }
}}
>
We&apos;re sunsetting Flowise.{' '}
<Link href='https://flowiseai.com/sunset' target='_blank' rel='noopener noreferrer'>
Learn more
</Link>
</Alert>
)

AnnouncementBanner.propTypes = {
onClose: PropTypes.func
}

export default AnnouncementBanner
46 changes: 1 addition & 45 deletions packages/ui/src/layout/MainLayout/Header/index.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import PropTypes from 'prop-types'
import { useSelector, useDispatch } from 'react-redux'
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'

// material-ui
import { Button, Avatar, Box, ButtonBase, Switch, Typography, Link } from '@mui/material'
Expand All @@ -12,10 +11,9 @@ import LogoSection from '../LogoSection'
import ProfileSection from './ProfileSection'
import WorkspaceSwitcher from '@/layout/MainLayout/Header/WorkspaceSwitcher'
import OrgWorkspaceBreadcrumbs from '@/layout/MainLayout/Header/OrgWorkspaceBreadcrumbs'
import PricingDialog from '@/ui-component/subscription/PricingDialog'

// assets
import { IconMenu2, IconX, IconSparkles } from '@tabler/icons-react'
import { IconMenu2, IconX } from '@tabler/icons-react'

// store
import { store } from '@/store'
Expand Down Expand Up @@ -144,17 +142,14 @@ GitHubStarButton.propTypes = {

const Header = ({ handleLeftDrawerToggle }) => {
const theme = useTheme()
const navigate = useNavigate()

const customization = useSelector((state) => state.customization)
const logoutApi = useApi(accountApi.logout)

const [isDark, setIsDark] = useState(customization.isDarkMode)
const dispatch = useDispatch()
const { isEnterpriseLicensed, isCloud, isOpenSource } = useConfig()
const currentUser = useSelector((state) => state.auth.user)
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated)
const [isPricingOpen, setIsPricingOpen] = useState(false)
const [starCount, setStarCount] = useState(0)

useNotifier()
Expand Down Expand Up @@ -270,45 +265,6 @@ const Header = ({ handleLeftDrawerToggle }) => {
)}
{isEnterpriseLicensed && isAuthenticated && <WorkspaceSwitcher />}
{isCloud && isAuthenticated && <OrgWorkspaceBreadcrumbs />}
{isCloud && currentUser?.isOrganizationAdmin && (
<Button
variant='contained'
sx={{
mr: 1,
ml: 2,
borderRadius: 15,
background: (theme) =>
`linear-gradient(90deg, ${theme.palette.primary.main} 10%, ${theme.palette.secondary.main} 100%)`,
color: (theme) => theme.palette.secondary.contrastText,
boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
transition: 'all 0.3s ease',
'&:hover': {
background: (theme) =>
`linear-gradient(90deg, ${darken(theme.palette.primary.main, 0.1)} 10%, ${darken(
theme.palette.secondary.main,
0.1
)} 100%)`,
boxShadow: '0 4px 8px rgba(0,0,0,0.3)'
}
}}
onClick={() => setIsPricingOpen(true)}
startIcon={<IconSparkles size={20} />}
>
Upgrade
</Button>
)}
{isPricingOpen && isCloud && (
<PricingDialog
open={isPricingOpen}
onClose={(planUpdated) => {
setIsPricingOpen(false)
if (planUpdated) {
navigate('/')
navigate(0)
}
}}
/>
)}
<MaterialUISwitch checked={isDark} onChange={changeDarkMode} />
<Box sx={{ ml: 2 }}></Box>
<ProfileSection handleLogout={signOutClicked} />
Expand Down
9 changes: 5 additions & 4 deletions packages/ui/src/layout/MainLayout/Sidebar/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { drawerWidth, headerHeight } from '@/store/constant'

// ==============================|| SIDEBAR DRAWER ||============================== //

const Sidebar = ({ drawerOpen, drawerToggle, window }) => {
const Sidebar = ({ drawerOpen, drawerToggle, window, bannerOffset = 0 }) => {
const theme = useTheme()
const matchUpMd = useMediaQuery(theme.breakpoints.up('md'))
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated)
Expand All @@ -40,7 +40,7 @@ const Sidebar = ({ drawerOpen, drawerToggle, window }) => {
<PerfectScrollbar
component='div'
style={{
height: !matchUpMd ? 'calc(100vh - 56px)' : `calc(100vh - ${headerHeight}px)`,
height: !matchUpMd ? 'calc(100vh - 56px)' : `calc(100vh - ${headerHeight + bannerOffset}px)`,
display: 'flex',
flexDirection: 'column'
}}
Expand Down Expand Up @@ -82,7 +82,7 @@ const Sidebar = ({ drawerOpen, drawerToggle, window }) => {
background: theme.palette.background.default,
color: theme.palette.text.primary,
[theme.breakpoints.up('md')]: {
top: `${headerHeight}px`
top: `${headerHeight + bannerOffset}px`
},
borderRight: drawerOpen ? '1px solid' : 'none',
borderColor: drawerOpen ? theme.palette.grey[900] + 25 : 'transparent'
Expand All @@ -101,7 +101,8 @@ const Sidebar = ({ drawerOpen, drawerToggle, window }) => {
Sidebar.propTypes = {
drawerOpen: PropTypes.bool,
drawerToggle: PropTypes.func,
window: PropTypes.object
window: PropTypes.object,
bannerOffset: PropTypes.number
}

export default Sidebar
Loading
Loading