Feature/Add Permissions To API Keys (#5590)

* feat(database.util.ts): add function to check if column exists in table

* feat(migrations): add permission column to apikey table

* chore(services/apikey): remove auto-create default API key

* chore: remove auto-create default API key

* fix(migrations): rename permission to permissions in apikey table

* feat(services/apikey): add permissions to create and update

* feat(views/apikey): add permissions to create and update

* feat(api): use API key permissions instead of role permissions

* feat(permissions): filter by user.permissions except for ROLE type

* feat(services/apikey): filter keys by user permissions in getAllApiKeys

* feat(services/apikey): limit API key permissions to user permissions during creation/update

* fix(database.util.ts): replace non-null assertion with explicit error handling

* feat(services/apikey): log errors for malformed API key permissions

* refactor(services/apikey): extract permission validation

* feat(permission): enhance permission filtering based on user features and platform type

* chore(migrations): lower API key permission value

* Update packages/ui/src/views/apikey/APIKeyDialog.jsx

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Update packages/ui/src/views/apikey/APIKeyDialog.jsx

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* refactor: remove deprecated APIKEY_PATH and APIKEY migration

* refactor(apikey): remove functionality related to import apikey

* feat(permission): filter out workspace and admin categories for non-ROLE

* feat(services/apikey): validate API key permissions during create and update

* chore(migrations): clean up role permissions values

* fix(migrations): handle corrupted permissions gracefully

* fix(server/src/index.ts): add error handling for API key permissions parsing

* fix(APIKeyDialog.jsx): handle corrupted permissions when editing API key

* fix(views/apikey/index.jsx): handle corrupted permissions in API key list

* fix(services/apikey): validate permissions JSON in API key operations

* Potential fix for code scanning alert no. 83: Clear-text logging of sensitive information

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* feat(migrations): change ApiKey.permissions column from TEXT to JSON

* Update packages/server/src/services/apikey/index.ts

Co-authored-by: Jocelyn Lin <[email protected]>

* feat(PricingDialog.jsx): handle downgrade with excessive apikey permissions

* feat(controllers\apikey): enforce permissions validation

* chore(services/apikey): remove redundant null checks

* fix(services/dataset): throw error when dataset not found before fetching rows

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Jocelyn Lin <[email protected]>
This commit is contained in:
yau-wd
2026-01-16 12:21:48 +08:00
committed by GitHub
co-authored by gemini-code-assist[bot] Copilot Autofix powered by AI Jocelyn Lin
parent d848baeb6b
commit e135b29433
35 changed files with 1139 additions and 817 deletions
-2
View File
@@ -1,7 +1,5 @@
PORT=3000
# APIKEY_PATH=/your_apikey_path/.flowise # (will be deprecated by end of 2025)
############################################################################################################
############################################## DATABASE ####################################################
############################################################################################################
-2
View File
@@ -1,7 +1,5 @@
WORKER_PORT=5566
# APIKEY_PATH=/your_apikey_path/.flowise # (will be deprecated by end of 2025)
############################################################################################################
############################################## DATABASE ####################################################
############################################################################################################
+10 -10
View File
@@ -1,13 +1,13 @@
global:
scrape_interval: 5s
scrape_interval: 5s
scrape_configs:
- job_name: "FlowiseAI"
static_configs:
- targets: ["localhost:8080","localhost:3000"]
- job_name: 'FlowiseAI'
static_configs:
- targets: ['localhost:8080', 'localhost:3000']
metrics_path: /api/v1/metrics/
scheme: http
authorization:
type: Bearer
credentials_file: '/etc/prometheus/api_key.txt'
metrics_path: /api/v1/metrics/
scheme: http
authorization:
type: Bearer
credentials_file: '/etc/prometheus/api_key.txt'
-2
View File
@@ -1,7 +1,5 @@
PORT=3000
# APIKEY_PATH=/your_apikey_path/.flowise # (will be deprecated by end of 2025)
############################################################################################################
############################################## DATABASE ####################################################
############################################################################################################
-9
View File
@@ -346,15 +346,6 @@ export interface IUploadFileSizeAndTypes {
maxUploadSize: number
}
export interface IApiKey {
id: string
keyName: string
apiKey: string
apiSecret: string
updatedDate: Date
workspaceId: string
}
export interface ICustomTemplate {
id: string
name: string
+34 -32
View File
@@ -1,5 +1,6 @@
import { Request, Response, NextFunction } from 'express'
import { NextFunction, Request, Response } from 'express'
import { StatusCodes } from 'http-status-codes'
import { LoggedInUser } from '../../enterprise/Interface.Enterprise'
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import apikeyService from '../../services/apikey'
import { getPageAndLimitParams } from '../../utils/pagination'
@@ -7,13 +8,14 @@ import { getPageAndLimitParams } from '../../utils/pagination'
// Get api keys
const getAllApiKeys = async (req: Request, res: Response, next: NextFunction) => {
try {
const autoCreateNewKey = true
const user = req.user as LoggedInUser
if (req.query?.type === 'organization' && user.isOrganizationAdmin)
return res.status(StatusCodes.OK).json(await apikeyService.getAllApiKeysByOrganization(user.activeOrganizationId))
const { page, limit } = getPageAndLimitParams(req)
if (!req.user?.activeWorkspaceId) {
throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Workspace ID is required`)
}
const apiResponse = await apikeyService.getAllApiKeys(req.user?.activeWorkspaceId, autoCreateNewKey, page, limit)
return res.json(apiResponse)
const apiResponse = await apikeyService.getAllApiKeys(user, page, limit)
return res.status(StatusCodes.OK).json(apiResponse)
} catch (error) {
next(error)
}
@@ -24,10 +26,19 @@ const createApiKey = async (req: Request, res: Response, next: NextFunction) =>
if (typeof req.body === 'undefined' || !req.body.keyName) {
throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Error: apikeyController.createApiKey - keyName not provided!`)
}
if (!req.user?.activeWorkspaceId) {
throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Workspace ID is required`)
if (
!req.body.permissions ||
!Array.isArray(req.body.permissions) ||
req.body.permissions.length === 0 ||
!req.body.permissions.every((p: any) => typeof p === 'string')
) {
throw new InternalFlowiseError(
StatusCodes.PRECONDITION_FAILED,
`Error: apikeyController.createApiKey - permissions must be an array of strings!`
)
}
const apiResponse = await apikeyService.createApiKey(req.body.keyName, req.user?.activeWorkspaceId)
const user = req.user as LoggedInUser
const apiResponse = await apikeyService.createApiKey(user, req.body.keyName, req.body.permissions)
return res.json(apiResponse)
} catch (error) {
next(error)
@@ -43,27 +54,19 @@ const updateApiKey = async (req: Request, res: Response, next: NextFunction) =>
if (typeof req.body === 'undefined' || !req.body.keyName) {
throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Error: apikeyController.updateApiKey - keyName not provided!`)
}
if (!req.user?.activeWorkspaceId) {
throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Workspace ID is required`)
if (
!req.body.permissions ||
!Array.isArray(req.body.permissions) ||
req.body.permissions.length === 0 ||
!req.body.permissions.every((p: any) => typeof p === 'string')
) {
throw new InternalFlowiseError(
StatusCodes.PRECONDITION_FAILED,
`Error: apikeyController.updateApiKey - permissions must be an array of strings!`
)
}
const apiResponse = await apikeyService.updateApiKey(req.params.id, req.body.keyName, req.user?.activeWorkspaceId)
return res.json(apiResponse)
} catch (error) {
next(error)
}
}
// Import Keys from JSON file
const importKeys = async (req: Request, res: Response, next: NextFunction) => {
try {
if (typeof req.body === 'undefined' || !req.body.jsonFile) {
throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Error: apikeyController.importKeys - body not provided!`)
}
if (!req.user?.activeWorkspaceId) {
throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Workspace ID is required`)
}
req.body.workspaceId = req.user?.activeWorkspaceId
const apiResponse = await apikeyService.importKeys(req.body)
const user = req.user as LoggedInUser
const apiResponse = await apikeyService.updateApiKey(user, req.params.id, req.body.keyName, req.body.permissions)
return res.json(apiResponse)
} catch (error) {
next(error)
@@ -104,6 +107,5 @@ export default {
deleteApiKey,
getAllApiKeys,
updateApiKey,
verifyApiKey,
importKeys
verifyApiKey
}
@@ -1,8 +1,7 @@
import { Column, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm'
import { IApiKey } from '../../Interface'
@Entity('apikey')
export class ApiKey implements IApiKey {
export class ApiKey {
@PrimaryColumn({ type: 'varchar', length: 20 })
id: string
@@ -15,6 +14,9 @@ export class ApiKey implements IApiKey {
@Column({ type: 'text' })
keyName: string
@Column({ nullable: false, type: 'simple-json', default: '[]' })
permissions: string[]
@Column({ type: 'timestamp' })
@UpdateDateColumn()
updatedDate: Date
@@ -0,0 +1,45 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
import { Role } from '../../../enterprise/database/entities/role.entity'
import { hasColumn } from '../../../utils/database.util'
import logger from '../../../utils/logger'
export class AddApiKeyPermission1765360298674 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const tableName = 'apikey'
const columnName = 'permissions'
const columnExists = await hasColumn(queryRunner, tableName, columnName)
if (!columnExists) {
await queryRunner.query(`ALTER TABLE \`${tableName}\` ADD COLUMN \`${columnName}\` JSON NOT NULL DEFAULT (JSON_ARRAY());`)
const permission =
'["chatflows:view","chatflows:create","chatflows:update","chatflows:duplicate","chatflows:delete","chatflows:export","chatflows:import","chatflows:config","chatflows:domains","agentflows:view","agentflows:create","agentflows:update","agentflows:duplicate","agentflows:delete","agentflows:export","agentflows:import","agentflows:config","agentflows:domains","tools:view","tools:create","tools:update","tools:delete","tools:export","assistants:view","assistants:create","assistants:update","assistants:delete","credentials:view","credentials:create","credentials:update","credentials:delete","variables:view","variables:create","variables:update","variables:delete","apikeys:view","apikeys:create","apikeys:update","apikeys:delete","documentStores:view","documentStores:create","documentStores:update","documentStores:delete","documentStores:add-loader","documentStores:delete-loader","documentStores:preview-process","documentStores:upsert-config","executions:view","executions:delete","templates:marketplace","templates:custom","templates:custom-delete","templates:toolexport","templates:flowexport"]'
await queryRunner.query(`UPDATE \`${tableName}\` SET \`${columnName}\` = '${permission}';`)
}
const sso = 'sso:manage'
const apikey = 'apikeys:import'
const itemsToRemove = [sso, apikey]
const roles: Role[] = await queryRunner.query(
`SELECT * FROM \`role\` WHERE \`${columnName}\` LIKE '%${sso}%' OR \`${columnName}\` LIKE '%${apikey}%';`
)
if (roles.length > 0) {
for (const role of roles) {
let permissions: string[] = []
try {
permissions = JSON.parse(role.permissions)
} catch (error) {
logger.error(`AddApiKeyPermission1765360298674 error parsing permissions for role ${role.id}:`, error)
continue
}
permissions = permissions.filter((permission: string) => !itemsToRemove.includes(permission))
await queryRunner.query(
`UPDATE \`role\` SET \`${columnName}\` = '${JSON.stringify(permissions)}' WHERE \`id\` = '${role.id}';`
)
}
}
}
public async down(): Promise<void> {}
}
@@ -41,6 +41,7 @@ import { ModifyChatflowType1755066758601 } from './1755066758601-ModifyChatflowT
import { AddTextToSpeechToChatFlow1759419231100 } from './1759419231100-AddTextToSpeechToChatFlow'
import { AddChatFlowNameIndex1759424809984 } from './1759424809984-AddChatFlowNameIndex'
import { FixDocumentStoreFileChunkLongText1765000000000 } from './1765000000000-FixDocumentStoreFileChunkLongText'
import { AddApiKeyPermission1765360298674 } from './1765360298674-AddApiKeyPermission'
import { AddAuthTables1720230151482 } from '../../../enterprise/database/migrations/mariadb/1720230151482-AddAuthTables'
import { AddWorkspace1725437498242 } from '../../../enterprise/database/migrations/mariadb/1725437498242-AddWorkspace'
@@ -108,5 +109,6 @@ export const mariadbMigrations = [
ModifyChatflowType1755066758601,
AddTextToSpeechToChatFlow1759419231100,
AddChatFlowNameIndex1759424809984,
FixDocumentStoreFileChunkLongText1765000000000
FixDocumentStoreFileChunkLongText1765000000000,
AddApiKeyPermission1765360298674
]
@@ -0,0 +1,45 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
import { Role } from '../../../enterprise/database/entities/role.entity'
import { hasColumn } from '../../../utils/database.util'
import logger from '../../../utils/logger'
export class AddApiKeyPermission1765360298674 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const tableName = 'apikey'
const columnName = 'permissions'
const columnExists = await hasColumn(queryRunner, tableName, columnName)
if (!columnExists) {
await queryRunner.query(`ALTER TABLE \`${tableName}\` ADD COLUMN \`${columnName}\` JSON NOT NULL DEFAULT (JSON_ARRAY());`)
const permission =
'["chatflows:view","chatflows:create","chatflows:update","chatflows:duplicate","chatflows:delete","chatflows:export","chatflows:import","chatflows:config","chatflows:domains","agentflows:view","agentflows:create","agentflows:update","agentflows:duplicate","agentflows:delete","agentflows:export","agentflows:import","agentflows:config","agentflows:domains","tools:view","tools:create","tools:update","tools:delete","tools:export","assistants:view","assistants:create","assistants:update","assistants:delete","credentials:view","credentials:create","credentials:update","credentials:delete","variables:view","variables:create","variables:update","variables:delete","apikeys:view","apikeys:create","apikeys:update","apikeys:delete","documentStores:view","documentStores:create","documentStores:update","documentStores:delete","documentStores:add-loader","documentStores:delete-loader","documentStores:preview-process","documentStores:upsert-config","executions:view","executions:delete","templates:marketplace","templates:custom","templates:custom-delete","templates:toolexport","templates:flowexport"]'
await queryRunner.query(`UPDATE \`${tableName}\` SET \`${columnName}\` = '${permission}';`)
}
const sso = 'sso:manage'
const apikey = 'apikeys:import'
const itemsToRemove = [sso, apikey]
const roles: Role[] = await queryRunner.query(
`SELECT * FROM \`role\` WHERE \`${columnName}\` LIKE '%${sso}%' OR \`${columnName}\` LIKE '%${apikey}%';`
)
if (roles.length > 0) {
for (const role of roles) {
let permissions: string[] = []
try {
permissions = JSON.parse(role.permissions)
} catch (error) {
logger.error(`AddApiKeyPermission1765360298674 error parsing permissions for role ${role.id}:`, error)
continue
}
permissions = permissions.filter((permission: string) => !itemsToRemove.includes(permission))
await queryRunner.query(
`UPDATE \`role\` SET \`${columnName}\` = '${JSON.stringify(permissions)}' WHERE \`id\` = '${role.id}';`
)
}
}
}
public async down(): Promise<void> {}
}
@@ -42,6 +42,7 @@ import { ModifyChatflowType1755066758601 } from './1755066758601-ModifyChatflowT
import { AddTextToSpeechToChatFlow1759419216034 } from './1759419216034-AddTextToSpeechToChatFlow'
import { AddChatFlowNameIndex1759424828558 } from './1759424828558-AddChatFlowNameIndex'
import { FixDocumentStoreFileChunkLongText1765000000000 } from './1765000000000-FixDocumentStoreFileChunkLongText'
import { AddApiKeyPermission1765360298674 } from './1765360298674-AddApiKeyPermission'
import { AddAuthTables1720230151482 } from '../../../enterprise/database/migrations/mysql/1720230151482-AddAuthTables'
import { AddWorkspace1720230151484 } from '../../../enterprise/database/migrations/mysql/1720230151484-AddWorkspace'
@@ -110,5 +111,6 @@ export const mysqlMigrations = [
ModifyChatflowType1755066758601,
AddTextToSpeechToChatFlow1759419216034,
AddChatFlowNameIndex1759424828558,
FixDocumentStoreFileChunkLongText1765000000000
FixDocumentStoreFileChunkLongText1765000000000,
AddApiKeyPermission1765360298674
]
@@ -0,0 +1,43 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
import { Role } from '../../../enterprise/database/entities/role.entity'
import { hasColumn } from '../../../utils/database.util'
import logger from '../../../utils/logger'
export class AddApiKeyPermission1765360298674 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const tableName = 'apikey'
const columnName = 'permissions'
const columnExists = await hasColumn(queryRunner, tableName, columnName)
if (!columnExists) {
await queryRunner.query(`ALTER TABLE "${tableName}" ADD COLUMN "${columnName}" JSONB NOT NULL DEFAULT '[]'::jsonb;`)
const permission =
'["chatflows:view","chatflows:create","chatflows:update","chatflows:duplicate","chatflows:delete","chatflows:export","chatflows:import","chatflows:config","chatflows:domains","agentflows:view","agentflows:create","agentflows:update","agentflows:duplicate","agentflows:delete","agentflows:export","agentflows:import","agentflows:config","agentflows:domains","tools:view","tools:create","tools:update","tools:delete","tools:export","assistants:view","assistants:create","assistants:update","assistants:delete","credentials:view","credentials:create","credentials:update","credentials:delete","variables:view","variables:create","variables:update","variables:delete","apikeys:view","apikeys:create","apikeys:update","apikeys:delete","documentStores:view","documentStores:create","documentStores:update","documentStores:delete","documentStores:add-loader","documentStores:delete-loader","documentStores:preview-process","documentStores:upsert-config","executions:view","executions:delete","templates:marketplace","templates:custom","templates:custom-delete","templates:toolexport","templates:flowexport"]'
await queryRunner.query(`UPDATE "${tableName}" SET "${columnName}" = '${permission}'::jsonb;`)
}
const sso = 'sso:manage'
const apikey = 'apikeys:import'
const itemsToRemove = [sso, apikey]
const roles: Role[] = await queryRunner.query(
`SELECT * FROM "role" WHERE "${columnName}" LIKE '%${sso}%' OR "${columnName}" LIKE '%${apikey}%';`
)
if (roles.length > 0) {
for (const role of roles) {
let permissions: string[] = []
try {
permissions = JSON.parse(role.permissions)
} catch (error) {
logger.error(`AddApiKeyPermission1765360298674 error parsing permissions for role ${role.id}:`, error)
continue
}
permissions = permissions.filter((permission: string) => !itemsToRemove.includes(permission))
await queryRunner.query(`UPDATE "role" SET "${columnName}" = '${JSON.stringify(permissions)}' WHERE "id" = '${role.id}';`)
}
}
}
public async down(): Promise<void> {}
}
@@ -40,6 +40,7 @@ import { AddTextToSpeechToChatFlow1754986480347 } from './1754986480347-AddTextT
import { ModifyChatflowType1755066758601 } from './1755066758601-ModifyChatflowType'
import { AddTextToSpeechToChatFlow1759419194331 } from './1759419194331-AddTextToSpeechToChatFlow'
import { AddChatFlowNameIndex1759424903973 } from './1759424903973-AddChatFlowNameIndex'
import { AddApiKeyPermission1765360298674 } from './1765360298674-AddApiKeyPermission'
import { AddAuthTables1720230151482 } from '../../../enterprise/database/migrations/postgres/1720230151482-AddAuthTables'
import { AddWorkspace1720230151484 } from '../../../enterprise/database/migrations/postgres/1720230151484-AddWorkspace'
@@ -106,5 +107,6 @@ export const postgresMigrations = [
AddTextToSpeechToChatFlow1754986480347,
ModifyChatflowType1755066758601,
AddTextToSpeechToChatFlow1759419194331,
AddChatFlowNameIndex1759424903973
AddChatFlowNameIndex1759424903973,
AddApiKeyPermission1765360298674
]
@@ -0,0 +1,43 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
import { Role } from '../../../enterprise/database/entities/role.entity'
import { hasColumn } from '../../../utils/database.util'
import logger from '../../../utils/logger'
export class AddApiKeyPermission1765360298674 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const tableName = 'apikey'
const columnName = 'permissions'
const columnExists = await hasColumn(queryRunner, tableName, columnName)
if (!columnExists) {
await queryRunner.query(`ALTER TABLE "${tableName}" ADD COLUMN "${columnName}" TEXT NOT NULL DEFAULT '[]';`)
const permission =
'["chatflows:view","chatflows:create","chatflows:update","chatflows:duplicate","chatflows:delete","chatflows:export","chatflows:import","chatflows:config","chatflows:domains","agentflows:view","agentflows:create","agentflows:update","agentflows:duplicate","agentflows:delete","agentflows:export","agentflows:import","agentflows:config","agentflows:domains","tools:view","tools:create","tools:update","tools:delete","tools:export","assistants:view","assistants:create","assistants:update","assistants:delete","credentials:view","credentials:create","credentials:update","credentials:delete","variables:view","variables:create","variables:update","variables:delete","apikeys:view","apikeys:create","apikeys:update","apikeys:delete","documentStores:view","documentStores:create","documentStores:update","documentStores:delete","documentStores:add-loader","documentStores:delete-loader","documentStores:preview-process","documentStores:upsert-config","executions:view","executions:delete","templates:marketplace","templates:custom","templates:custom-delete","templates:toolexport","templates:flowexport"]'
await queryRunner.query(`UPDATE "${tableName}" SET "${columnName}" = '${permission}';`)
}
const sso = 'sso:manage'
const apikey = 'apikeys:import'
const itemsToRemove = [sso, apikey]
const roles: Role[] = await queryRunner.query(
`SELECT * FROM "role" WHERE "${columnName}" LIKE '%${sso}%' OR "${columnName}" LIKE '%${apikey}%';`
)
if (roles.length > 0) {
for (const role of roles) {
let permissions: string[] = []
try {
permissions = JSON.parse(role.permissions)
} catch (error) {
logger.error(`AddApiKeyPermission1765360298674 error parsing permissions for role ${role.id}:`, error)
continue
}
permissions = permissions.filter((permission: string) => !itemsToRemove.includes(permission))
await queryRunner.query(`UPDATE "role" SET "${columnName}" = '${JSON.stringify(permissions)}' WHERE "id" = '${role.id}';`)
}
}
}
public async down(): Promise<void> {}
}
@@ -38,6 +38,7 @@ import { AddTextToSpeechToChatFlow1754986486669 } from './1754986486669-AddTextT
import { ModifyChatflowType1755066758601 } from './1755066758601-ModifyChatflowType'
import { AddTextToSpeechToChatFlow1759419136055 } from './1759419136055-AddTextToSpeechToChatFlow'
import { AddChatFlowNameIndex1759424923093 } from './1759424923093-AddChatFlowNameIndex'
import { AddApiKeyPermission1765360298674 } from './1765360298674-AddApiKeyPermission'
import { AddAuthTables1720230151482 } from '../../../enterprise/database/migrations/sqlite/1720230151482-AddAuthTables'
import { AddWorkspace1720230151484 } from '../../../enterprise/database/migrations/sqlite/1720230151484-AddWorkspace'
@@ -102,5 +103,6 @@ export const sqliteMigrations = [
AddTextToSpeechToChatFlow1754986486669,
ModifyChatflowType1755066758601,
AddTextToSpeechToChatFlow1759419136055,
AddChatFlowNameIndex1759424923093
AddChatFlowNameIndex1759424923093,
AddApiKeyPermission1765360298674
]
@@ -72,7 +72,7 @@ export type LoggedInUser = {
activeWorkspaceId: string
activeWorkspace: string
assignedWorkspaces: IAssignedWorkspace[]
permissions?: string[]
permissions: string[]
features?: Record<string, string>
ssoRefreshToken?: string
ssoToken?: string
@@ -1,10 +1,97 @@
import { NextFunction, Request, Response } from 'express'
import { StatusCodes } from 'http-status-codes'
import { Platform } from '../../../Interface'
import { getRunningExpressApp } from '../../../utils/getRunningExpressApp'
import { LoggedInUser } from '../../Interface.Enterprise'
const getAllPermissions = async (req: Request, res: Response, next: NextFunction) => {
try {
const appServer = getRunningExpressApp()
return res.json(appServer.identityManager.getPermissions())
const type = req.params.type as string
const allPermissions = appServer.identityManager.getPermissions().toJSON()
const user = req.user as LoggedInUser
let permissions: { [key: string]: { key: string; value: string }[] } = allPermissions
// Mapping of feature flags to permission prefixes
const featureToPermissionMap: { [key: string]: string[] } = {
'feat:login-activity': ['loginActivity:'],
'feat:logs': ['logs:'],
'feat:roles': ['roles:'],
'feat:share': ['credentials:share', 'templates:custom-share'],
'feat:sso-config': ['sso:'],
'feat:users': ['users:'],
'feat:workspaces': ['workspace:']
}
// Category filtering for non-ROLE type
if (type !== 'ROLE') {
const filteredPermissions: { [key: string]: { key: string; value: string }[] } = {}
for (const [category, categoryPermissions] of Object.entries(allPermissions)) {
// Exclude workspace and admin categories
if (category !== 'workspace' && category !== 'admin') {
filteredPermissions[category] = categoryPermissions
}
}
permissions = filteredPermissions
}
// Feature-based filtering for Cloud platform
if (type !== 'ROLE' && appServer.identityManager.getPlatformType() === Platform.CLOUD) {
const userFeatures = user.features
if (userFeatures) {
const disabledFeatures = Object.entries(userFeatures).filter(([, value]) => value === 'false')
// Get list of disabled permission prefixes
const disabledPermissionPrefixes: string[] = []
disabledFeatures.forEach(([featureKey]) => {
const prefixes = featureToPermissionMap[featureKey]
if (prefixes) {
disabledPermissionPrefixes.push(...prefixes)
}
})
// Filter out permissions based on disabled features
const filteredPermissions: { [key: string]: { key: string; value: string }[] } = {}
for (const [category, categoryPermissions] of Object.entries(permissions)) {
const filteredCategoryPermissions = (categoryPermissions as any[]).filter((permission) => {
// Check if this permission starts with any disabled prefix
const isDisabled = disabledPermissionPrefixes.some((prefix) => permission.key.startsWith(prefix))
return !isDisabled
})
// Only include category if it has remaining permissions
if (filteredCategoryPermissions.length > 0) {
filteredPermissions[category] = filteredCategoryPermissions
}
}
permissions = filteredPermissions
}
}
// User-level filtering for non-admin users
if (type !== 'ROLE' && user.isOrganizationAdmin === false) {
const userPermissions = user.permissions as string[]
const filteredPermissions: { [key: string]: { key: string; value: string }[] } = {}
for (const [category, categoryPermissions] of Object.entries(permissions)) {
const filteredCategoryPermissions = (categoryPermissions as any[]).filter((permission) =>
userPermissions?.includes(permission.key)
)
if (filteredCategoryPermissions.length > 0) {
filteredPermissions[category] = filteredCategoryPermissions
}
}
permissions = filteredPermissions
}
return res.status(StatusCodes.OK).json(permissions)
} catch (error) {
next(error)
}
@@ -6,138 +6,139 @@ export class Permissions {
// this.categories.push(auditCategory)
const chatflowsCategory = new PermissionCategory('chatflows')
chatflowsCategory.addPermission(new Permission('chatflows:view', 'View'))
chatflowsCategory.addPermission(new Permission('chatflows:create', 'Create'))
chatflowsCategory.addPermission(new Permission('chatflows:update', 'Update'))
chatflowsCategory.addPermission(new Permission('chatflows:duplicate', 'Duplicate'))
chatflowsCategory.addPermission(new Permission('chatflows:delete', 'Delete'))
chatflowsCategory.addPermission(new Permission('chatflows:export', 'Export'))
chatflowsCategory.addPermission(new Permission('chatflows:import', 'Import'))
chatflowsCategory.addPermission(new Permission('chatflows:config', 'Edit Configuration'))
chatflowsCategory.addPermission(new Permission('chatflows:domains', 'Allowed Domains'))
chatflowsCategory.addPermission(new Permission('chatflows:view', 'View', true, true, true))
chatflowsCategory.addPermission(new Permission('chatflows:create', 'Create', true, true, true))
chatflowsCategory.addPermission(new Permission('chatflows:update', 'Update', true, true, true))
chatflowsCategory.addPermission(new Permission('chatflows:duplicate', 'Duplicate', true, true, true))
chatflowsCategory.addPermission(new Permission('chatflows:delete', 'Delete', true, true, true))
chatflowsCategory.addPermission(new Permission('chatflows:export', 'Export', true, true, true))
chatflowsCategory.addPermission(new Permission('chatflows:import', 'Import', true, true, true))
chatflowsCategory.addPermission(new Permission('chatflows:config', 'Edit Configuration', true, true, true))
chatflowsCategory.addPermission(new Permission('chatflows:domains', 'Allowed Domains', true, true, true))
this.categories.push(chatflowsCategory)
const agentflowsCategory = new PermissionCategory('agentflows')
agentflowsCategory.addPermission(new Permission('agentflows:view', 'View'))
agentflowsCategory.addPermission(new Permission('agentflows:create', 'Create'))
agentflowsCategory.addPermission(new Permission('agentflows:update', 'Update'))
agentflowsCategory.addPermission(new Permission('agentflows:duplicate', 'Duplicate'))
agentflowsCategory.addPermission(new Permission('agentflows:delete', 'Delete'))
agentflowsCategory.addPermission(new Permission('agentflows:export', 'Export'))
agentflowsCategory.addPermission(new Permission('agentflows:import', 'Import'))
agentflowsCategory.addPermission(new Permission('agentflows:config', 'Edit Configuration'))
agentflowsCategory.addPermission(new Permission('agentflows:domains', 'Allowed Domains'))
agentflowsCategory.addPermission(new Permission('agentflows:view', 'View', true, true, true))
agentflowsCategory.addPermission(new Permission('agentflows:create', 'Create', true, true, true))
agentflowsCategory.addPermission(new Permission('agentflows:update', 'Update', true, true, true))
agentflowsCategory.addPermission(new Permission('agentflows:duplicate', 'Duplicate', true, true, true))
agentflowsCategory.addPermission(new Permission('agentflows:delete', 'Delete', true, true, true))
agentflowsCategory.addPermission(new Permission('agentflows:export', 'Export', true, true, true))
agentflowsCategory.addPermission(new Permission('agentflows:import', 'Import', true, true, true))
agentflowsCategory.addPermission(new Permission('agentflows:config', 'Edit Configuration', true, true, true))
agentflowsCategory.addPermission(new Permission('agentflows:domains', 'Allowed Domains', true, true, true))
this.categories.push(agentflowsCategory)
const toolsCategory = new PermissionCategory('tools')
toolsCategory.addPermission(new Permission('tools:view', 'View'))
toolsCategory.addPermission(new Permission('tools:create', 'Create'))
toolsCategory.addPermission(new Permission('tools:update', 'Update'))
toolsCategory.addPermission(new Permission('tools:delete', 'Delete'))
toolsCategory.addPermission(new Permission('tools:export', 'Export'))
toolsCategory.addPermission(new Permission('tools:view', 'View', true, true, true))
toolsCategory.addPermission(new Permission('tools:create', 'Create', true, true, true))
toolsCategory.addPermission(new Permission('tools:update', 'Update', true, true, true))
toolsCategory.addPermission(new Permission('tools:delete', 'Delete', true, true, true))
toolsCategory.addPermission(new Permission('tools:export', 'Export', true, true, true))
this.categories.push(toolsCategory)
const assistantsCategory = new PermissionCategory('assistants')
assistantsCategory.addPermission(new Permission('assistants:view', 'View'))
assistantsCategory.addPermission(new Permission('assistants:create', 'Create'))
assistantsCategory.addPermission(new Permission('assistants:update', 'Update'))
assistantsCategory.addPermission(new Permission('assistants:delete', 'Delete'))
assistantsCategory.addPermission(new Permission('assistants:view', 'View', true, true, true))
assistantsCategory.addPermission(new Permission('assistants:create', 'Create', true, true, true))
assistantsCategory.addPermission(new Permission('assistants:update', 'Update', true, true, true))
assistantsCategory.addPermission(new Permission('assistants:delete', 'Delete', true, true, true))
this.categories.push(assistantsCategory)
const credentialsCategory = new PermissionCategory('credentials')
credentialsCategory.addPermission(new Permission('credentials:view', 'View'))
credentialsCategory.addPermission(new Permission('credentials:create', 'Create'))
credentialsCategory.addPermission(new Permission('credentials:update', 'Update'))
credentialsCategory.addPermission(new Permission('credentials:delete', 'Delete'))
credentialsCategory.addPermission(new Permission('credentials:share', 'Share'))
credentialsCategory.addPermission(new Permission('credentials:view', 'View', true, true, true))
credentialsCategory.addPermission(new Permission('credentials:create', 'Create', true, true, true))
credentialsCategory.addPermission(new Permission('credentials:update', 'Update', true, true, true))
credentialsCategory.addPermission(new Permission('credentials:delete', 'Delete', true, true, true))
credentialsCategory.addPermission(new Permission('credentials:share', 'Share', false, true, true))
this.categories.push(credentialsCategory)
const variablesCategory = new PermissionCategory('variables')
variablesCategory.addPermission(new Permission('variables:view', 'View'))
variablesCategory.addPermission(new Permission('variables:create', 'Create'))
variablesCategory.addPermission(new Permission('variables:update', 'Update'))
variablesCategory.addPermission(new Permission('variables:delete', 'Delete'))
variablesCategory.addPermission(new Permission('variables:view', 'View', true, true, true))
variablesCategory.addPermission(new Permission('variables:create', 'Create', true, true, true))
variablesCategory.addPermission(new Permission('variables:update', 'Update', true, true, true))
variablesCategory.addPermission(new Permission('variables:delete', 'Delete', true, true, true))
this.categories.push(variablesCategory)
const apikeysCategory = new PermissionCategory('apikeys')
apikeysCategory.addPermission(new Permission('apikeys:view', 'View'))
apikeysCategory.addPermission(new Permission('apikeys:create', 'Create'))
apikeysCategory.addPermission(new Permission('apikeys:update', 'Update'))
apikeysCategory.addPermission(new Permission('apikeys:delete', 'Delete'))
apikeysCategory.addPermission(new Permission('apikeys:import', 'Import'))
apikeysCategory.addPermission(new Permission('apikeys:view', 'View', true, true, true))
apikeysCategory.addPermission(new Permission('apikeys:create', 'Create', true, true, true))
apikeysCategory.addPermission(new Permission('apikeys:update', 'Update', true, true, true))
apikeysCategory.addPermission(new Permission('apikeys:delete', 'Delete', true, true, true))
this.categories.push(apikeysCategory)
const documentStoresCategory = new PermissionCategory('documentStores')
documentStoresCategory.addPermission(new Permission('documentStores:view', 'View'))
documentStoresCategory.addPermission(new Permission('documentStores:create', 'Create'))
documentStoresCategory.addPermission(new Permission('documentStores:update', 'Update'))
documentStoresCategory.addPermission(new Permission('documentStores:delete', 'Delete Document Store'))
documentStoresCategory.addPermission(new Permission('documentStores:add-loader', 'Add Document Loader'))
documentStoresCategory.addPermission(new Permission('documentStores:delete-loader', 'Delete Document Loader'))
documentStoresCategory.addPermission(new Permission('documentStores:preview-process', 'Preview & Process Document Chunks'))
documentStoresCategory.addPermission(new Permission('documentStores:upsert-config', 'Upsert Config'))
documentStoresCategory.addPermission(new Permission('documentStores:view', 'View', true, true, true))
documentStoresCategory.addPermission(new Permission('documentStores:create', 'Create', true, true, true))
documentStoresCategory.addPermission(new Permission('documentStores:update', 'Update', true, true, true))
documentStoresCategory.addPermission(new Permission('documentStores:delete', 'Delete Document Store', true, true, true))
documentStoresCategory.addPermission(new Permission('documentStores:add-loader', 'Add Document Loader', true, true, true))
documentStoresCategory.addPermission(new Permission('documentStores:delete-loader', 'Delete Document Loader', true, true, true))
documentStoresCategory.addPermission(
new Permission('documentStores:preview-process', 'Preview & Process Document Chunks', true, true, true)
)
documentStoresCategory.addPermission(new Permission('documentStores:upsert-config', 'Upsert Config', true, true, true))
this.categories.push(documentStoresCategory)
const datasetsCategory = new PermissionCategory('datasets')
datasetsCategory.addPermission(new Permission('datasets:view', 'View'))
datasetsCategory.addPermission(new Permission('datasets:create', 'Create'))
datasetsCategory.addPermission(new Permission('datasets:update', 'Update'))
datasetsCategory.addPermission(new Permission('datasets:delete', 'Delete'))
datasetsCategory.addPermission(new Permission('datasets:view', 'View', false, true, true))
datasetsCategory.addPermission(new Permission('datasets:create', 'Create', false, true, true))
datasetsCategory.addPermission(new Permission('datasets:update', 'Update', false, true, true))
datasetsCategory.addPermission(new Permission('datasets:delete', 'Delete', false, true, true))
this.categories.push(datasetsCategory)
const executionsCategory = new PermissionCategory('executions')
executionsCategory.addPermission(new Permission('executions:view', 'View'))
executionsCategory.addPermission(new Permission('executions:delete', 'Delete'))
executionsCategory.addPermission(new Permission('executions:view', 'View', true, true, true))
executionsCategory.addPermission(new Permission('executions:delete', 'Delete', true, true, true))
this.categories.push(executionsCategory)
const evaluatorsCategory = new PermissionCategory('evaluators')
evaluatorsCategory.addPermission(new Permission('evaluators:view', 'View'))
evaluatorsCategory.addPermission(new Permission('evaluators:create', 'Create'))
evaluatorsCategory.addPermission(new Permission('evaluators:update', 'Update'))
evaluatorsCategory.addPermission(new Permission('evaluators:delete', 'Delete'))
evaluatorsCategory.addPermission(new Permission('evaluators:view', 'View', false, true, true))
evaluatorsCategory.addPermission(new Permission('evaluators:create', 'Create', false, true, true))
evaluatorsCategory.addPermission(new Permission('evaluators:update', 'Update', false, true, true))
evaluatorsCategory.addPermission(new Permission('evaluators:delete', 'Delete', false, true, true))
this.categories.push(evaluatorsCategory)
const evaluationsCategory = new PermissionCategory('evaluations')
evaluationsCategory.addPermission(new Permission('evaluations:view', 'View'))
evaluationsCategory.addPermission(new Permission('evaluations:create', 'Create'))
evaluationsCategory.addPermission(new Permission('evaluations:update', 'Update'))
evaluationsCategory.addPermission(new Permission('evaluations:delete', 'Delete'))
evaluationsCategory.addPermission(new Permission('evaluations:run', 'Run Again'))
evaluationsCategory.addPermission(new Permission('evaluations:view', 'View', false, true, true))
evaluationsCategory.addPermission(new Permission('evaluations:create', 'Create', false, true, true))
evaluationsCategory.addPermission(new Permission('evaluations:update', 'Update', false, true, true))
evaluationsCategory.addPermission(new Permission('evaluations:delete', 'Delete', false, true, true))
evaluationsCategory.addPermission(new Permission('evaluations:run', 'Run Again', false, true, true))
this.categories.push(evaluationsCategory)
const templatesCategory = new PermissionCategory('templates')
templatesCategory.addPermission(new Permission('templates:marketplace', 'View Marketplace Templates'))
templatesCategory.addPermission(new Permission('templates:custom', 'View Custom Templates'))
templatesCategory.addPermission(new Permission('templates:custom-delete', 'Delete Custom Template'))
templatesCategory.addPermission(new Permission('templates:toolexport', 'Export Tool as Template'))
templatesCategory.addPermission(new Permission('templates:flowexport', 'Export Flow as Template'))
templatesCategory.addPermission(new Permission('templates:custom-share', 'Share Custom Templates'))
templatesCategory.addPermission(new Permission('templates:marketplace', 'View Marketplace Templates', true, true, true))
templatesCategory.addPermission(new Permission('templates:custom', 'View Custom Templates', true, true, true))
templatesCategory.addPermission(new Permission('templates:custom-delete', 'Delete Custom Template', true, true, true))
templatesCategory.addPermission(new Permission('templates:toolexport', 'Export Tool as Template', true, true, true))
templatesCategory.addPermission(new Permission('templates:flowexport', 'Export Flow as Template', true, true, true))
templatesCategory.addPermission(new Permission('templates:custom-share', 'Share Custom Templates', false, true, true))
this.categories.push(templatesCategory)
const workspaceCategory = new PermissionCategory('workspace')
workspaceCategory.addPermission(new Permission('workspace:view', 'View'))
workspaceCategory.addPermission(new Permission('workspace:create', 'Create'))
workspaceCategory.addPermission(new Permission('workspace:update', 'Update'))
workspaceCategory.addPermission(new Permission('workspace:add-user', 'Add User'))
workspaceCategory.addPermission(new Permission('workspace:unlink-user', 'Remove User'))
workspaceCategory.addPermission(new Permission('workspace:delete', 'Delete'))
workspaceCategory.addPermission(new Permission('workspace:export', 'Export Data within Workspace'))
workspaceCategory.addPermission(new Permission('workspace:import', 'Import Data within Workspace'))
workspaceCategory.addPermission(new Permission('workspace:view', 'View', false, true, true))
workspaceCategory.addPermission(new Permission('workspace:create', 'Create', false, true, true))
workspaceCategory.addPermission(new Permission('workspace:update', 'Update', false, true, true))
workspaceCategory.addPermission(new Permission('workspace:add-user', 'Add User', false, true, true))
workspaceCategory.addPermission(new Permission('workspace:unlink-user', 'Remove User', false, true, true))
workspaceCategory.addPermission(new Permission('workspace:delete', 'Delete', false, true, true))
workspaceCategory.addPermission(new Permission('workspace:export', 'Export Data within Workspace', false, true, true))
workspaceCategory.addPermission(new Permission('workspace:import', 'Import Data within Workspace', false, true, true))
this.categories.push(workspaceCategory)
const adminCategory = new PermissionCategory('admin')
adminCategory.addPermission(new Permission('users:manage', 'Manage Users'))
adminCategory.addPermission(new Permission('roles:manage', 'Manage Roles'))
adminCategory.addPermission(new Permission('sso:manage', 'Manage SSO'))
adminCategory.addPermission(new Permission('users:manage', 'Manage Users', false, true, true))
adminCategory.addPermission(new Permission('roles:manage', 'Manage Roles', false, true, true))
adminCategory.addPermission(new Permission('sso:manage', 'Manage SSO', false, true, false))
this.categories.push(adminCategory)
const logsCategory = new PermissionCategory('logs')
logsCategory.addPermission(new Permission('logs:view', 'View Logs', true))
logsCategory.addPermission(new Permission('logs:view', 'View Logs', false, true, false))
this.categories.push(logsCategory)
const loginActivityCategory = new PermissionCategory('loginActivity')
loginActivityCategory.addPermission(new Permission('loginActivity:view', 'View Login Activity', true))
loginActivityCategory.addPermission(new Permission('loginActivity:delete', 'Delete Login Activity', true))
loginActivityCategory.addPermission(new Permission('loginActivity:view', 'View Login Activity', false, true, false))
loginActivityCategory.addPermission(new Permission('loginActivity:delete', 'Delete Login Activity', false, true, false))
this.categories.push(loginActivityCategory)
}
@@ -167,13 +168,21 @@ export class PermissionCategory {
}
export class Permission {
constructor(public name: string, public description: string, public isEnterprise: boolean = false) {}
constructor(
public name: string,
public description: string,
public isOpenSource: boolean = false,
public isEnterprise: boolean = false,
public isCloud: boolean = false
) {}
public toJSON() {
return {
key: this.name,
value: this.description,
isEnterprise: this.isEnterprise
isOpenSource: this.isOpenSource,
isEnterprise: this.isEnterprise,
isCloud: this.isCloud
}
}
}
@@ -3,7 +3,7 @@ import authController from '../../controllers/auth'
const router = express.Router()
// RBAC
router.get(['/', '/permissions'], authController.getAllPermissions)
router.get(['/:type', '/permissions/:type'], authController.getAllPermissions)
router.get(['/sso-success'], authController.ssoSuccess)
+32 -46
View File
@@ -1,40 +1,38 @@
import express, { Request, Response } from 'express'
import path from 'path'
import cors from 'cors'
import http from 'http'
import { ExpressAdapter } from '@bull-board/express'
import cookieParser from 'cookie-parser'
import { DataSource, IsNull } from 'typeorm'
import { MODE, Platform } from './Interface'
import { getNodeModulesPackagePath, getEncryptionKey } from './utils'
import logger, { expressRequestLogger } from './utils/logger'
import { getDataSource } from './DataSource'
import { NodesPool } from './NodesPool'
import { ChatFlow } from './database/entities/ChatFlow'
import { CachePool } from './CachePool'
import cors from 'cors'
import express, { Request, Response } from 'express'
import 'global-agent/bootstrap'
import http from 'http'
import path from 'path'
import { DataSource } from 'typeorm'
import { AbortControllerPool } from './AbortControllerPool'
import { RateLimiterManager } from './utils/rateLimit'
import { getAllowedIframeOrigins, getCorsOptions, sanitizeMiddleware } from './utils/XSS'
import { Telemetry } from './utils/telemetry'
import flowiseApiV1Router from './routes'
import errorHandlerMiddleware from './middlewares/errors'
import { WHITELIST_URLS } from './utils/constants'
import { CachePool } from './CachePool'
import { ChatFlow } from './database/entities/ChatFlow'
import { getDataSource } from './DataSource'
import { Organization } from './enterprise/database/entities/organization.entity'
import { Workspace } from './enterprise/database/entities/workspace.entity'
import { LoggedInUser } from './enterprise/Interface.Enterprise'
import { initializeJwtCookieMiddleware, verifyToken, verifyTokenForBullMQDashboard } from './enterprise/middleware/passport'
import { IdentityManager } from './IdentityManager'
import { SSEStreamer } from './utils/SSEStreamer'
import { validateAPIKey } from './utils/validateKey'
import { LoggedInUser } from './enterprise/Interface.Enterprise'
import { MODE, Platform } from './Interface'
import { IMetricsProvider } from './Interface.Metrics'
import { Prometheus } from './metrics/Prometheus'
import { OpenTelemetry } from './metrics/OpenTelemetry'
import { Prometheus } from './metrics/Prometheus'
import errorHandlerMiddleware from './middlewares/errors'
import { NodesPool } from './NodesPool'
import { QueueManager } from './queue/QueueManager'
import { RedisEventSubscriber } from './queue/RedisEventSubscriber'
import 'global-agent/bootstrap'
import flowiseApiV1Router from './routes'
import { UsageCacheManager } from './UsageCacheManager'
import { Workspace } from './enterprise/database/entities/workspace.entity'
import { Organization } from './enterprise/database/entities/organization.entity'
import { GeneralRole, Role } from './enterprise/database/entities/role.entity'
import { migrateApiKeysFromJsonToDb } from './utils/apiKey'
import { ExpressAdapter } from '@bull-board/express'
import { getEncryptionKey, getNodeModulesPackagePath } from './utils'
import { WHITELIST_URLS } from './utils/constants'
import logger, { expressRequestLogger } from './utils/logger'
import { RateLimiterManager } from './utils/rateLimit'
import { SSEStreamer } from './utils/SSEStreamer'
import { Telemetry } from './utils/telemetry'
import { validateAPIKey } from './utils/validateKey'
import { getAllowedIframeOrigins, getCorsOptions, sanitizeMiddleware } from './utils/XSS'
declare global {
namespace Express {
@@ -148,9 +146,6 @@ export class App {
logger.info('🔗 [server]: Redis event subscriber connected successfully')
}
// TODO: Remove this by end of 2025
await migrateApiKeysFromJsonToDb(this.AppDataSource, this.identityManager.getPlatformType())
logger.info('🎉 [server]: All initialization steps completed successfully!')
} catch (error) {
logger.error('❌ [server]: Error during Data Source initialization:', error)
@@ -236,27 +231,19 @@ export class App {
}
}
const { isValid, workspaceId: apiKeyWorkSpaceId } = await validateAPIKey(req)
if (!isValid) {
const { isValid, apiKey } = await validateAPIKey(req)
if (!isValid || !apiKey) {
return res.status(401).json({ error: 'Unauthorized Access' })
}
// Find workspace
const workspace = await this.AppDataSource.getRepository(Workspace).findOne({
where: { id: apiKeyWorkSpaceId }
where: { id: apiKey.workspaceId }
})
if (!workspace) {
return res.status(401).json({ error: 'Unauthorized Access' })
}
// Find owner role
const ownerRole = await this.AppDataSource.getRepository(Role).findOne({
where: { name: GeneralRole.OWNER, organizationId: IsNull() }
})
if (!ownerRole) {
return res.status(401).json({ error: 'Unauthorized Access' })
}
// Find organization
const activeOrganizationId = workspace.organizationId as string
const org = await this.AppDataSource.getRepository(Organization).findOne({
@@ -269,17 +256,16 @@ export class App {
const customerId = org.customerId as string
const features = await this.identityManager.getFeaturesByPlan(subscriptionId)
const productId = await this.identityManager.getProductIdFromSubscription(subscriptionId)
// @ts-ignore
req.user = {
permissions: [...JSON.parse(ownerRole.permissions)],
permissions: apiKey.permissions,
features,
activeOrganizationId: activeOrganizationId,
activeOrganizationSubscriptionId: subscriptionId,
activeOrganizationCustomerId: customerId,
activeOrganizationProductId: productId,
isOrganizationAdmin: true,
activeWorkspaceId: apiKeyWorkSpaceId!,
isOrganizationAdmin: false,
activeWorkspaceId: workspace.id,
activeWorkspace: workspace.name
}
next()
@@ -5,7 +5,6 @@ const router = express.Router()
// CREATE
router.post('/', checkPermission('apikeys:create'), apikeyController.createApiKey)
router.post('/import', checkPermission('apikeys:import'), apikeyController.importKeys)
// READ
router.get('/', checkPermission('apikeys:view'), apikeyController.getAllApiKeys)
+160 -168
View File
@@ -1,41 +1,141 @@
import { StatusCodes } from 'http-status-codes'
import { generateAPIKey, generateSecretHash } from '../../utils/apiKey'
import { addChatflowsCount } from '../../utils/addChatflowsCount'
import { v4 as uuidv4 } from 'uuid'
import { ApiKey } from '../../database/entities/ApiKey'
import { LoggedInUser } from '../../enterprise/Interface.Enterprise'
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { getErrorMessage } from '../../errors/utils'
import { Platform } from '../../Interface'
import { addChatflowsCount } from '../../utils/addChatflowsCount'
import { generateAPIKey, generateSecretHash } from '../../utils/apiKey'
import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
import { ApiKey } from '../../database/entities/ApiKey'
import { Not, IsNull } from 'typeorm'
import { getWorkspaceSearchOptions } from '../../enterprise/utils/ControllerServiceUtils'
import { v4 as uuidv4 } from 'uuid'
import logger from '../../utils/logger'
const getAllApiKeysFromDB = async (workspaceId: string, page: number = -1, limit: number = -1) => {
const appServer = getRunningExpressApp()
const queryBuilder = appServer.AppDataSource.getRepository(ApiKey).createQueryBuilder('api_key').orderBy('api_key.updatedDate', 'DESC')
if (page > 0 && limit > 0) {
queryBuilder.skip((page - 1) * limit)
queryBuilder.take(limit)
/**
* Validates that requested permissions are allowed for API keys
* @param user - The logged-in user
* @param permissions - string array of requested permissions
* @param operation - The operation being performed (for error message)
* @throws InternalFlowiseError if validation fails
*/
function validatePermissions(user: LoggedInUser, requestedPermissions: string[], operation: string) {
// API Keys should not have workspace or admin permissions
// This applies to ALL users, including admins (platform constraint)
const hasRestrictedPermissions = requestedPermissions.some(
(permission: string) => permission.startsWith('workspace:') || permission.startsWith('admin:')
)
if (hasRestrictedPermissions) {
throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, `Cannot ${operation} API key with workspace or admin permissions`)
}
queryBuilder.andWhere('api_key.workspaceId = :workspaceId', { workspaceId })
const [data, total] = await queryBuilder.getManyAndCount()
const keysWithChatflows = await addChatflowsCount(data)
if (page > 0 && limit > 0) {
return { total, data: keysWithChatflows }
} else {
return keysWithChatflows
// For Cloud platform, check feature-gated permissions
// This also applies to ALL users, including admins (platform constraint)
const appServer = getRunningExpressApp()
if (appServer.identityManager.getPlatformType() === Platform.CLOUD) {
if (!user.features) {
// On Cloud platform, user features should always exist
// Log the anomaly with context for debugging
logger.error(
`[server]: Missing user features on Cloud platform for ${operation} API key. ` +
`User: ${user.email || user.id}, ` +
`Organization: ${user.activeOrganizationId || 'unknown'}, ` +
`Subscription: ${user.activeOrganizationSubscriptionId || 'unknown'}, ` +
`Customer: ${user.activeOrganizationCustomerId || 'unknown'}, ` +
`Workspace: ${user.activeWorkspaceId || 'unknown'}`
)
throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Unable to validate permissions: user features not available`)
}
const featureToPermissionMap: { [key: string]: string[] } = {
'feat:login-activity': ['loginActivity:'],
'feat:logs': ['logs:'],
'feat:roles': ['roles:'],
'feat:share': ['credentials:share', 'templates:custom-share'],
'feat:sso-config': ['sso:'],
'feat:users': ['users:'],
'feat:workspaces': ['workspace:']
}
const disabledFeatures = Object.entries(user.features).filter(([, value]) => value === 'false')
const disabledPermissionPrefixes: string[] = []
disabledFeatures.forEach(([featureKey]) => {
const prefixes = featureToPermissionMap[featureKey]
if (prefixes) {
disabledPermissionPrefixes.push(...prefixes)
}
})
const hasDisabledFeaturePermissions = requestedPermissions.some((permission: string) =>
disabledPermissionPrefixes.some((prefix) => permission.startsWith(prefix))
)
if (hasDisabledFeaturePermissions) {
throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, `Cannot ${operation} API key with permissions for disabled features`)
}
}
// User permission validation - only applies to non-admins (authorization check)
if (!user.isOrganizationAdmin) {
// Check if all requested permissions are included in user permissions
const hasInvalidPermissions = requestedPermissions.some((permission: string) => !user.permissions.includes(permission))
if (hasInvalidPermissions) {
throw new InternalFlowiseError(
StatusCodes.BAD_REQUEST,
`Cannot ${operation} API key with permissions that exceed your own permissions`
)
}
}
}
const getAllApiKeys = async (workspaceId: string, autoCreateNewKey?: boolean, page: number = -1, limit: number = -1) => {
/**
* Get all API keys for an organization
* Returns all API keys across all workspaces in the organization
*/
async function getAllApiKeysByOrganization(organizationId: string): Promise<ApiKey[]> {
const appServer = getRunningExpressApp()
const ApiKeys = await appServer.AppDataSource.getRepository(ApiKey)
.createQueryBuilder('api_key')
.select(['api_key.keyName', 'api_key.permissions'])
.leftJoin('workspace', 'workspace', 'api_key.workspaceId = workspace.id')
.where('workspace.organizationId = :organizationId', { organizationId })
.getMany()
return ApiKeys
}
/**
* Get all API keys for a workspace
* Non-admin users can only view API keys whose permissions are a subset of their own permissions
*/
const getAllApiKeys = async (user: LoggedInUser, page: number = -1, limit: number = -1) => {
try {
let keys = await getAllApiKeysFromDB(workspaceId, page, limit)
const isEmpty = keys?.total === 0 || (Array.isArray(keys) && keys?.length === 0)
if (isEmpty && autoCreateNewKey) {
await createApiKey('DefaultKey', workspaceId)
keys = await getAllApiKeysFromDB(workspaceId, page, limit)
const appServer = getRunningExpressApp()
const queryBuilder = appServer.AppDataSource.getRepository(ApiKey)
.createQueryBuilder('api_key')
.orderBy('api_key.updatedDate', 'DESC')
if (page > 0 && limit > 0) {
queryBuilder.skip((page - 1) * limit)
queryBuilder.take(limit)
}
queryBuilder.andWhere('api_key.workspaceId = :workspaceId', { workspaceId: user.activeWorkspaceId })
const allKeys = await queryBuilder.getMany()
// Filter keys based on user permissions
let filteredKeys = allKeys
if (!user.isOrganizationAdmin) {
// Non-admin users can only see API keys whose permissions are a subset of their own
filteredKeys = allKeys.filter((key) => {
// Check if all key permissions are included in user permissions
return key.permissions.every((permission: string) => user.permissions.includes(permission))
})
}
const keysWithChatflows = await addChatflowsCount(filteredKeys)
if (page > 0 && limit > 0) {
return { total: filteredKeys.length, data: keysWithChatflows }
} else {
return keysWithChatflows
}
return keys
} catch (error) {
throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: apikeyService.getAllApiKeys - ${getErrorMessage(error)}`)
}
@@ -71,42 +171,42 @@ const getApiKeyById = async (apiKeyId: string) => {
}
}
const createApiKey = async (keyName: string, workspaceId: string) => {
try {
const apiKey = generateAPIKey()
const apiSecret = generateSecretHash(apiKey)
const appServer = getRunningExpressApp()
const newKey = new ApiKey()
newKey.id = uuidv4()
newKey.apiKey = apiKey
newKey.apiSecret = apiSecret
newKey.keyName = keyName
newKey.workspaceId = workspaceId
const key = appServer.AppDataSource.getRepository(ApiKey).create(newKey)
await appServer.AppDataSource.getRepository(ApiKey).save(key)
return await getAllApiKeysFromDB(workspaceId)
} catch (error) {
throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: apikeyService.createApiKey - ${getErrorMessage(error)}`)
}
const createApiKey = async (user: LoggedInUser, keyName: string, permissions: string[]) => {
// Validate permissions before creating the key
validatePermissions(user, permissions, 'create')
const apiKey = generateAPIKey()
const apiSecret = generateSecretHash(apiKey)
const appServer = getRunningExpressApp()
const newKey = new ApiKey()
newKey.id = uuidv4()
newKey.apiKey = apiKey
newKey.apiSecret = apiSecret
newKey.keyName = keyName
newKey.permissions = permissions
newKey.workspaceId = user.activeWorkspaceId
const key = appServer.AppDataSource.getRepository(ApiKey).create(newKey)
await appServer.AppDataSource.getRepository(ApiKey).save(key)
return await getAllApiKeys(user)
}
// Update api key
const updateApiKey = async (id: string, keyName: string, workspaceId: string) => {
try {
const appServer = getRunningExpressApp()
const currentKey = await appServer.AppDataSource.getRepository(ApiKey).findOneBy({
id: id,
workspaceId: workspaceId
})
if (!currentKey) {
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `ApiKey ${currentKey} not found`)
}
currentKey.keyName = keyName
await appServer.AppDataSource.getRepository(ApiKey).save(currentKey)
return await getAllApiKeysFromDB(workspaceId)
} catch (error) {
throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: apikeyService.updateApiKey - ${getErrorMessage(error)}`)
const updateApiKey = async (user: LoggedInUser, id: string, keyName: string, permissions: string[]) => {
// Validate permissions before updating the key
validatePermissions(user, permissions, 'update')
const appServer = getRunningExpressApp()
const currentKey = await appServer.AppDataSource.getRepository(ApiKey).findOneBy({
id: id,
workspaceId: user.activeWorkspaceId
})
if (!currentKey) {
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `ApiKey ${currentKey} not found`)
}
currentKey.keyName = keyName
currentKey.permissions = permissions
await appServer.AppDataSource.getRepository(ApiKey).save(currentKey)
return await getAllApiKeys(user)
}
const deleteApiKey = async (id: string, workspaceId: string) => {
@@ -122,114 +222,6 @@ const deleteApiKey = async (id: string, workspaceId: string) => {
}
}
const importKeys = async (body: any) => {
try {
const jsonFile = body.jsonFile
const workspaceId = body.workspaceId
const splitDataURI = jsonFile.split(',')
if (splitDataURI[0] !== 'data:application/json;base64') {
throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Invalid dataURI`)
}
const bf = Buffer.from(splitDataURI[1] || '', 'base64')
const plain = bf.toString('utf8')
const keys = JSON.parse(plain)
// Validate schema of imported keys
if (!Array.isArray(keys)) {
throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, `Invalid format: Expected an array of API keys`)
}
const requiredFields = ['keyName', 'apiKey', 'apiSecret', 'createdAt', 'id']
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
if (typeof key !== 'object' || key === null) {
throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, `Invalid format: Key at index ${i} is not an object`)
}
for (const field of requiredFields) {
if (!(field in key)) {
throw new InternalFlowiseError(
StatusCodes.BAD_REQUEST,
`Invalid format: Key at index ${i} is missing required field '${field}'`
)
}
if (typeof key[field] !== 'string') {
throw new InternalFlowiseError(
StatusCodes.BAD_REQUEST,
`Invalid format: Key at index ${i} field '${field}' must be a string`
)
}
if (key[field].trim() === '') {
throw new InternalFlowiseError(
StatusCodes.BAD_REQUEST,
`Invalid format: Key at index ${i} field '${field}' cannot be empty`
)
}
}
}
const appServer = getRunningExpressApp()
const allApiKeys = await appServer.AppDataSource.getRepository(ApiKey).findBy(getWorkspaceSearchOptions(workspaceId))
if (body.importMode === 'replaceAll') {
await appServer.AppDataSource.getRepository(ApiKey).delete({
id: Not(IsNull()),
workspaceId: workspaceId
})
}
if (body.importMode === 'errorIfExist') {
// if importMode is errorIfExist, check for existing keys and raise error before any modification to the DB
for (const key of keys) {
const keyNameExists = allApiKeys.find((k) => k.keyName === key.keyName)
if (keyNameExists) {
throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Key with name ${key.keyName} already exists`)
}
}
}
// iterate through the keys and add them to the database
for (const key of keys) {
const keyNameExists = allApiKeys.find((k) => k.keyName === key.keyName)
if (keyNameExists) {
const keyIndex = allApiKeys.findIndex((k) => k.keyName === key.keyName)
switch (body.importMode) {
case 'overwriteIfExist':
case 'replaceAll': {
const currentKey = allApiKeys[keyIndex]
currentKey.id = uuidv4()
currentKey.apiKey = key.apiKey
currentKey.apiSecret = key.apiSecret
currentKey.workspaceId = workspaceId
await appServer.AppDataSource.getRepository(ApiKey).save(currentKey)
break
}
case 'ignoreIfExist': {
// ignore this key and continue
continue
}
case 'errorIfExist': {
// should not reach here as we have already checked for existing keys
throw new Error(`Key with name ${key.keyName} already exists`)
}
default: {
throw new Error(`Unknown overwrite option ${body.importMode}`)
}
}
} else {
const newKey = new ApiKey()
newKey.id = uuidv4()
newKey.apiKey = key.apiKey
newKey.apiSecret = key.apiSecret
newKey.keyName = key.keyName
newKey.workspaceId = workspaceId
const newKeyEntity = appServer.AppDataSource.getRepository(ApiKey).create(newKey)
await appServer.AppDataSource.getRepository(ApiKey).save(newKeyEntity)
}
}
return await getAllApiKeysFromDB(workspaceId)
} catch (error) {
throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: apikeyService.importKeys - ${getErrorMessage(error)}`)
}
}
const verifyApiKey = async (paramApiKey: string): Promise<string> => {
try {
const appServer = getRunningExpressApp()
@@ -256,9 +248,9 @@ export default {
createApiKey,
deleteApiKey,
getAllApiKeys,
getAllApiKeysByOrganization,
updateApiKey,
verifyApiKey,
getApiKey,
getApiKeyById,
importKeys
getApiKeyById
}
@@ -1,11 +1,11 @@
import { StatusCodes } from 'http-status-codes'
import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { getErrorMessage } from '../../errors/utils'
import { Dataset } from '../../database/entities/Dataset'
import { DatasetRow } from '../../database/entities/DatasetRow'
import { Readable } from 'stream'
import { In } from 'typeorm'
import { Dataset } from '../../database/entities/Dataset'
import { DatasetRow } from '../../database/entities/DatasetRow'
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { getErrorMessage } from '../../errors/utils'
import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
import csv from 'csv-parser'
@@ -50,6 +50,7 @@ const getDataset = async (id: string, workspaceId: string, page: number = -1, li
id: id,
workspaceId: workspaceId
})
if (!dataset) throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `Dataset ${id} not found`)
const queryBuilder = appServer.AppDataSource.getRepository(DatasetRow).createQueryBuilder('dsr').orderBy('dsr.sequenceNo', 'ASC')
queryBuilder.andWhere('dsr.datasetId = :datasetId', { datasetId: id })
if (page > 0 && limit > 0) {
-123
View File
@@ -1,22 +1,4 @@
import { randomBytes, scryptSync, timingSafeEqual } from 'crypto'
import { ICommonObject } from 'flowise-components'
import fs from 'fs'
import path from 'path'
import { DataSource } from 'typeorm'
import { ApiKey } from '../database/entities/ApiKey'
import { Workspace } from '../enterprise/database/entities/workspace.entity'
import { v4 as uuidv4 } from 'uuid'
import { ChatFlow } from '../database/entities/ChatFlow'
import { addChatflowsCount } from './addChatflowsCount'
import { Platform } from '../Interface'
/**
* Returns the api key path
* @returns {string}
*/
export const getAPIKeyPath = (): string => {
return process.env.APIKEY_PATH ? path.join(process.env.APIKEY_PATH, 'api.json') : path.join(__dirname, '..', '..', 'api.json')
}
/**
* Generate the api key
@@ -49,108 +31,3 @@ export const compareKeys = (storedKey: string, suppliedKey: string): boolean =>
const buffer = scryptSync(suppliedKey, salt, 64) as Buffer
return timingSafeEqual(Buffer.from(hashedPassword, 'hex'), buffer)
}
/**
* Get API keys
* @returns {Promise<ICommonObject[]>}
*/
export const getAPIKeys = async (): Promise<ICommonObject[]> => {
try {
const content = await fs.promises.readFile(getAPIKeyPath(), 'utf8')
return JSON.parse(content)
} catch (error) {
return []
}
}
/**
* Get API Key details
* @param {string} apiKey
* @returns {Promise<ICommonObject[]>}
*/
export const getApiKey = async (apiKey: string) => {
const existingAPIKeys = await getAPIKeys()
const keyIndex = existingAPIKeys.findIndex((key) => key.apiKey === apiKey)
if (keyIndex < 0) return undefined
return existingAPIKeys[keyIndex]
}
export const migrateApiKeysFromJsonToDb = async (appDataSource: DataSource, platformType: Platform) => {
if (platformType === Platform.CLOUD) {
return
}
if (!process.env.APIKEY_STORAGE_TYPE || process.env.APIKEY_STORAGE_TYPE === 'json') {
const keys = await getAPIKeys()
if (keys.length > 0) {
try {
// Get all available workspaces
const workspaces = await appDataSource.getRepository(Workspace).find()
for (const key of keys) {
const existingKey = await appDataSource.getRepository(ApiKey).findOneBy({
apiKey: key.apiKey
})
// Only add if key doesn't already exist in DB
if (!existingKey) {
// Create a new API key for each workspace
if (workspaces.length > 0) {
for (const workspace of workspaces) {
const newKey = new ApiKey()
newKey.id = uuidv4()
newKey.apiKey = key.apiKey
newKey.apiSecret = key.apiSecret
newKey.keyName = key.keyName
newKey.workspaceId = workspace.id
const keyEntity = appDataSource.getRepository(ApiKey).create(newKey)
await appDataSource.getRepository(ApiKey).save(keyEntity)
const chatflows = await appDataSource.getRepository(ChatFlow).findBy({
apikeyid: key.id,
workspaceId: workspace.id
})
for (const chatflow of chatflows) {
chatflow.apikeyid = newKey.id
await appDataSource.getRepository(ChatFlow).save(chatflow)
}
await addChatflowsCount(chatflows)
}
} else {
// If no workspaces exist, create the key without a workspace ID and later will be updated by setNullWorkspaceId
const newKey = new ApiKey()
newKey.id = uuidv4()
newKey.apiKey = key.apiKey
newKey.apiSecret = key.apiSecret
newKey.keyName = key.keyName
const keyEntity = appDataSource.getRepository(ApiKey).create(newKey)
await appDataSource.getRepository(ApiKey).save(keyEntity)
const chatflows = await appDataSource.getRepository(ChatFlow).findBy({
apikeyid: key.id
})
for (const chatflow of chatflows) {
chatflow.apikeyid = newKey.id
await appDataSource.getRepository(ChatFlow).save(chatflow)
}
await addChatflowsCount(chatflows)
}
}
}
// Delete the JSON file
if (fs.existsSync(getAPIKeyPath())) {
fs.unlinkSync(getAPIKeyPath())
}
} catch (error) {
console.error('Error migrating API keys from JSON to DB', error)
}
}
}
}
@@ -0,0 +1,13 @@
import { QueryRunner } from 'typeorm'
export async function hasColumn(queryRunner: QueryRunner, tableName: string, columnName: string): Promise<boolean> {
const table = await queryRunner.getTable(tableName)
if (!table) {
throw new Error(`Table ${tableName} not found`)
}
const hasColumn = table.columns.some((column) => column.name === columnName)
return hasColumn
}
+4 -4
View File
@@ -40,9 +40,9 @@ export const validateFlowAPIKey = async (req: Request, chatflow: ChatFlow): Prom
/**
* Validate and Get API Key Information
* @param {Request} req
* @returns {Promise<{isValid: boolean, apiKey?: ApiKey, workspaceId?: string}>}
* @returns {Promise<{isValid: boolean, apiKey?: ApiKey}>}
*/
export const validateAPIKey = async (req: Request): Promise<{ isValid: boolean; apiKey?: ApiKey; workspaceId?: string }> => {
export const validateAPIKey = async (req: Request): Promise<{ isValid: boolean; apiKey?: ApiKey }> => {
const authorizationHeader = (req.headers['Authorization'] as string) ?? (req.headers['authorization'] as string) ?? ''
if (!authorizationHeader) return { isValid: false }
@@ -58,10 +58,10 @@ export const validateAPIKey = async (req: Request): Promise<{ isValid: boolean;
const apiSecret = apiKey.apiSecret
if (!apiSecret || !compareKeys(apiSecret, suppliedKey)) {
return { isValid: false, apiKey, workspaceId: apiKey.workspaceId }
return { isValid: false, apiKey }
}
return { isValid: true, apiKey, workspaceId: apiKey.workspaceId }
return { isValid: true, apiKey }
} catch (error) {
return { isValid: false }
}
+1 -4
View File
@@ -8,12 +8,9 @@ const updateAPI = (id, body) => client.put(`/apikey/${id}`, body)
const deleteAPI = (id) => client.delete(`/apikey/${id}`)
const importAPI = (body) => client.post(`/apikey/import`, body)
export default {
getAllAPIKeys,
createNewAPI,
updateAPI,
deleteAPI,
importAPI
deleteAPI
}
+1 -1
View File
@@ -5,7 +5,7 @@ const resolveLogin = (body) => client.post(`/auth/resolve`, body)
const login = (body) => client.post(`/auth/login`, body)
// permissions
const getAllPermissions = () => client.get(`/auth/permissions`)
const getAllPermissions = (type) => client.get(`/auth/permissions/${type}`)
const ssoSuccess = (token) => client.get(`/auth/sso-success?token=${token}`)
export default {
@@ -1,28 +1,29 @@
import { useState, useEffect, useMemo } from 'react'
import { useSelector } from 'react-redux'
import PropTypes from 'prop-types'
import accountApi from '@/api/account.api'
import apiKeyApi from '@/api/apikey'
import pricingApi from '@/api/pricing'
import userApi from '@/api/user'
import workspaceApi from '@/api/workspace'
import useApi from '@/hooks/useApi'
import { store } from '@/store'
import { upgradePlanSuccess } from '@/store/reducers/authSlice'
import {
Box,
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Grid,
Typography,
Button,
IconButton,
Box,
CircularProgress,
DialogActions
Typography
} from '@mui/material'
import { IconX, IconCheck, IconCreditCard, IconExternalLink, IconAlertCircle } from '@tabler/icons-react'
import { useTheme, alpha } from '@mui/material/styles'
import accountApi from '@/api/account.api'
import pricingApi from '@/api/pricing'
import workspaceApi from '@/api/workspace'
import userApi from '@/api/user'
import useApi from '@/hooks/useApi'
import { alpha, useTheme } from '@mui/material/styles'
import { IconAlertCircle, IconCheck, IconCreditCard, IconExternalLink, IconX } from '@tabler/icons-react'
import { useSnackbar } from 'notistack'
import { store } from '@/store'
import { upgradePlanSuccess } from '@/store/reducers/authSlice'
import PropTypes from 'prop-types'
import { useEffect, useMemo, useState } from 'react'
import { useSelector } from 'react-redux'
const PricingDialog = ({ open, onClose }) => {
const customization = useSelector((state) => state.customization)
@@ -37,6 +38,7 @@ const PricingDialog = ({ open, onClose }) => {
const [purchasedSeats, setPurchasedSeats] = useState(0)
const [occupiedSeats, setOccupiedSeats] = useState(0)
const [workspaceCount, setWorkspaceCount] = useState(0)
const [proAPIKeysCount, setProAPIKeysCount] = useState(0)
const [isOpeningBillingPortal, setIsOpeningBillingPortal] = useState(false)
const getPricingPlansApi = useApi(pricingApi.getPricingPlans)
@@ -44,6 +46,7 @@ const PricingDialog = ({ open, onClose }) => {
const getPlanProrationApi = useApi(userApi.getPlanProration)
const getAdditionalSeatsQuantityApi = useApi(userApi.getAdditionalSeatsQuantity)
const getAllWorkspacesApi = useApi(workspaceApi.getAllWorkspacesByOrganizationId)
const getAllAPIKeysApi = useApi(apiKeyApi.getAllAPIKeys)
useEffect(() => {
getPricingPlansApi.request()
@@ -115,6 +118,20 @@ const PricingDialog = ({ open, onClose }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getAllWorkspacesApi.data])
useEffect(() => {
if (getAllAPIKeysApi.data) {
if (getAllAPIKeysApi.data?.length > 0) {
// Count API keys that have sharing permissions
const sharingKeysCount = getAllAPIKeysApi.data.filter((apiKey) => {
return apiKey.permissions.includes('credentials:share') || apiKey.permissions.includes('templates:custom-share')
}).length
setProAPIKeysCount(sharingKeysCount)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getAllAPIKeysApi.data])
useEffect(() => {
if (
getCustomerDefaultSourceApi.data &&
@@ -162,6 +179,7 @@ const PricingDialog = ({ open, onClose }) => {
if (isCurrentPlanValue && (plan.title === 'Pro' || plan.title === 'Enterprise')) {
getAllWorkspacesApi.request(currentUser?.activeOrganizationId)
getAllAPIKeysApi.request({ type: 'organization' })
}
return {
@@ -420,6 +438,22 @@ const PricingDialog = ({ open, onClose }) => {
You must remove all workspaces except the default workspace before changing your plan.
</Typography>
</>
) : proAPIKeysCount > 0 ? (
<>
<Typography
color='error'
sx={{
p: 2,
borderRadius: 1,
display: 'flex',
alignItems: 'center',
gap: 1
}}
>
<IconAlertCircle size={20} />
You must remove all API keys with sharing permissions before changing your plan.
</Typography>
</>
) : (
<>
{getCustomerDefaultSourceApi.loading ? (
@@ -643,7 +677,8 @@ const PricingDialog = ({ open, onClose }) => {
!prorationInfo ||
purchasedSeats > 0 ||
occupiedSeats > 1 ||
workspaceCount > 1
workspaceCount > 1 ||
proAPIKeysCount > 0
}
>
{isUpdatingPlan ? (
@@ -0,0 +1,98 @@
.apikey-editor {
padding: 20px 0px;
border-radius: 10px;
width: 100%;
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
gap: 20px;
height: 75vh;
}
.key-name {
position: sticky;
top: 0;
z-index: 1;
}
.permissions-container > p,
.key-name label {
display: block;
font-weight: bold;
margin: 0;
margin-bottom: 5px;
}
.key-name input {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 14px;
display: block;
}
.permissions-container {
overflow-y: hidden;
max-height: calc(100vh - 120px);
}
.permissions-list-wrapper {
overflow-y: auto;
max-height: 100%;
padding-right: 10px;
padding-bottom: 10px;
}
.permission-category {
margin-bottom: 20px;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 15px;
}
.category-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #e0e0e0;
padding-bottom: 10px;
margin-bottom: 10px;
}
.category-header h3 {
margin: 0;
font-size: 16px;
}
.category-header button {
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
padding: 5px 10px;
cursor: pointer;
font-size: 14px;
}
.permissions-list {
display: flex;
flex-wrap: wrap;
margin-top: 10px;
}
.permission-item {
width: 50%;
box-sizing: border-box;
}
.permission-item label {
font-size: 14px;
display: flex;
align-items: center;
padding: 5px 0;
}
.permission-item input {
margin-right: 10px;
}
+279 -27
View File
@@ -1,39 +1,51 @@
import { createPortal } from 'react-dom'
import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions'
import PropTypes from 'prop-types'
import { useState, useEffect } from 'react'
import { useEffect, useState } from 'react'
import { createPortal } from 'react-dom'
import { useDispatch } from 'react-redux'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'
import { StyledButton } from '@/ui-component/button/StyledButton'
import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
import {
Box,
Typography,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Stack,
IconButton,
OutlinedInput,
Popover
Popover,
Stack,
Typography
} from '@mui/material'
import { useTheme } from '@mui/material/styles'
import { StyledButton } from '@/ui-component/button/StyledButton'
// Icons
import { IconX, IconCopy } from '@tabler/icons-react'
import { IconCopy, IconKey, IconX } from '@tabler/icons-react'
// API
import apikeyApi from '@/api/apikey'
import authApi from '@/api/auth'
// Hooks
import useApi from '@/hooks/useApi'
import { useConfig } from '@/store/context/ConfigContext'
// utils
import useNotifier from '@/utils/useNotifier'
// const
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions'
import './APIKeyDialog.css'
const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) => {
const portalElement = document.getElementById('portal')
const theme = useTheme()
const dispatch = useDispatch()
const { isOpenSource, isEnterpriseLicensed, isCloud } = useConfig()
// ==============================|| Snackbar ||============================== //
@@ -45,6 +57,10 @@ const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) => {
const [keyName, setKeyName] = useState('')
const [anchorEl, setAnchorEl] = useState(null)
const openPopOver = Boolean(anchorEl)
const [selectedPermissions, setSelectedPermissions] = useState({})
const [permissions, setPermissions] = useState({})
const getAllPermissionsApi = useApi(authApi.getAllPermissions)
useEffect(() => {
if (dialogProps.type === 'EDIT' && dialogProps.key) {
@@ -52,15 +68,161 @@ const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) => {
} else if (dialogProps.type === 'ADD') {
setKeyName('')
}
getAllPermissionsApi.request('API_KEY')
return () => {
setSelectedPermissions({})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dialogProps])
useEffect(() => {
if (getAllPermissionsApi.error) {
if (setError) setError(getAllPermissionsApi.error)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getAllPermissionsApi.error])
useEffect(() => {
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
else dispatch({ type: HIDE_CANVAS_DIALOG })
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
}, [show, dispatch])
useEffect(() => {
if (getAllPermissionsApi.data) {
const permissionsData = getAllPermissionsApi.data
// Filter permissions based on current platform
Object.keys(permissionsData).forEach((category) => {
permissionsData[category] = permissionsData[category].filter((permission) => {
if (isOpenSource) return permission.isOpenSource
if (isEnterpriseLicensed) return permission.isEnterprise
if (isCloud) return permission.isCloud
return false
})
})
// Remove categories that have no permissions left
Object.keys(permissionsData).forEach((category) => {
if (permissionsData[category].length === 0) {
delete permissionsData[category]
}
})
setPermissions(permissionsData)
if (dialogProps.type === 'EDIT' && dialogProps.key) {
const keyPermissions = dialogProps.key.permissions || []
if (keyPermissions && keyPermissions.length > 0) {
const tempSelectedPermissions = {}
Object.keys(permissionsData).forEach((category) => {
permissionsData[category].forEach((permission) => {
keyPermissions.forEach((perm) => {
if (perm === permission.key) {
if (!tempSelectedPermissions[category]) {
tempSelectedPermissions[category] = {}
}
tempSelectedPermissions[category][perm] = true
}
})
})
})
setSelectedPermissions(tempSelectedPermissions)
}
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getAllPermissionsApi.data])
const handleClosePopOver = () => {
setAnchorEl(null)
}
const handlePermissionChange = (category, key) => {
setSelectedPermissions((prevPermissions) => {
const updatedCategoryPermissions = {
...prevPermissions[category],
[key]: !prevPermissions[category]?.[key]
}
if (category === 'templates') {
if (key !== 'templates:marketplace' && key !== 'templates:custom') {
updatedCategoryPermissions['templates:marketplace'] = true
updatedCategoryPermissions['templates:custom'] = true
}
} else {
const viewPermissionKey = `${category}:view`
if (key !== viewPermissionKey) {
const hasEnabledPermissions = Object.entries(updatedCategoryPermissions).some(
([permissionKey, isEnabled]) => permissionKey !== viewPermissionKey && isEnabled
)
if (hasEnabledPermissions) {
updatedCategoryPermissions[viewPermissionKey] = true
}
} else {
const hasEnabledPermissions = Object.entries(updatedCategoryPermissions).some(
([permissionKey, isEnabled]) => permissionKey === viewPermissionKey && isEnabled
)
if (hasEnabledPermissions) {
updatedCategoryPermissions[key] = true
}
}
}
return {
...prevPermissions,
[category]: updatedCategoryPermissions
}
})
}
const isCheckboxDisabled = (permissions, category, key) => {
if (category === 'templates') {
// For templates, disable marketplace and custom view if any other permission is enabled
if (key === 'templates:marketplace' || key === 'templates:custom') {
return Object.entries(permissions[category] || {}).some(
([permKey, isEnabled]) => permKey !== 'templates:marketplace' && permKey !== 'templates:custom' && isEnabled
)
}
} else {
const viewPermissionKey = `${category}:view`
if (key === viewPermissionKey) {
// Disable the view permission if any other permission is enabled
return Object.entries(permissions[category] || {}).some(
([permKey, isEnabled]) => permKey !== viewPermissionKey && isEnabled
)
}
}
// Non-view permissions are never disabled
return false
}
const handleSelectAll = (category) => {
const allSelected = permissions[category].every((permission) => selectedPermissions[category]?.[permission.key])
setSelectedPermissions((prevPermissions) => ({
...prevPermissions,
[category]: Object.fromEntries(permissions[category].map((permission) => [permission.key, !allSelected]))
}))
}
const addNewKey = async () => {
try {
const createResp = await apikeyApi.createNewAPI({ keyName })
const tempPermissions = Object.keys(selectedPermissions)
.map((category) => {
return Object.keys(selectedPermissions[category]).map((key) => {
if (selectedPermissions[category][key]) {
return key
}
})
})
.flat()
.filter(Boolean)
const createResp = await apikeyApi.createNewAPI({
keyName,
permissions: tempPermissions
})
if (createResp.data) {
enqueueSnackbar({
message: 'New API key added',
@@ -99,7 +261,21 @@ const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) => {
const saveKey = async () => {
try {
const saveResp = await apikeyApi.updateAPI(dialogProps.key.id, { keyName })
const tempPermissions = Object.keys(selectedPermissions)
.map((category) => {
return Object.keys(selectedPermissions[category]).map((key) => {
if (selectedPermissions[category][key]) {
return key
}
})
})
.flat()
.filter(Boolean)
const saveResp = await apikeyApi.updateAPI(dialogProps.key.id, {
keyName,
permissions: tempPermissions
})
if (saveResp.data) {
enqueueSnackbar({
message: 'API Key saved',
@@ -136,19 +312,46 @@ const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) => {
}
}
const checkDisabled = () => {
if (!keyName || keyName === '') {
return true
}
if (!Object.keys(selectedPermissions).length || !ifPermissionContainsTrue(selectedPermissions)) {
return true
}
return false
}
const ifPermissionContainsTrue = (obj) => {
for (const key in obj) {
if (typeof obj[key] === 'object' && obj[key] !== null) {
// Recursively check nested objects
if (ifPermissionContainsTrue(obj[key])) {
return true
}
} else if (obj[key] === true) {
return true
}
}
return false
}
const component = show ? (
<Dialog
fullWidth
maxWidth='sm'
maxWidth='md'
open={show}
onClose={onCancel}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
{dialogProps.title}
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<IconKey style={{ marginRight: '10px' }} />
{dialogProps.title}
</div>
</DialogTitle>
<DialogContent>
<DialogContent sx={{ backgroundColor: 'transparent' }}>
{dialogProps.type === 'EDIT' && (
<Box sx={{ p: 2 }}>
<Typography variant='overline'>API Key</Typography>
@@ -199,23 +402,71 @@ const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) => {
</Box>
)}
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>Key Name</Typography>
</Stack>
<OutlinedInput
id='keyName'
type='string'
fullWidth
placeholder='My New Key'
value={keyName}
name='keyName'
onChange={(e) => setKeyName(e.target.value)}
/>
</Box>
<div className='apikey-editor'>
<Box>
<Typography sx={{ mb: 1 }} variant='h5'>
<span style={{ color: 'red' }}>*&nbsp;&nbsp;</span>Key Name
</Typography>
<OutlinedInput
id='keyName'
type='string'
size='small'
fullWidth
placeholder='My New Key'
value={keyName}
name='keyName'
onChange={(e) => setKeyName(e.target.value)}
/>
</Box>
<div className='permissions-container'>
<p>
<span style={{ color: 'red' }}>*&nbsp;&nbsp;</span>Permissions
</p>
<div className='permissions-list-wrapper'>
{permissions &&
Object.keys(permissions).map((category) => (
<div key={category} className='permission-category'>
<div className='category-header'>
<h3>
{category
.replace(/([A-Z])/g, ' $1')
.trim()
.toUpperCase()}
</h3>
<button type='button' onClick={() => handleSelectAll(category)}>
Select All
</button>
</div>
<div className='permissions-list'>
{permissions[category].map((permission, index) => (
<div
key={permission.key}
className={`permission-item ${index % 2 === 0 ? 'left-column' : 'right-column'}`}
>
<label>
<input
type='checkbox'
checked={selectedPermissions[category]?.[permission.key] || false}
disabled={isCheckboxDisabled(selectedPermissions, category, permission.key)}
onChange={() => handlePermissionChange(category, permission.key)}
/>
{permission.value}
</label>
</div>
))}
</div>
</div>
))}
</div>
</div>
</div>
</DialogContent>
<DialogActions>
<Button variant='outlined' onClick={onCancel}>
Cancel
</Button>
<StyledButton
disabled={checkDisabled()}
variant='contained'
onClick={() => (dialogProps.type === 'ADD' ? addNewKey() : saveKey())}
id={dialogProps.customBtnId}
@@ -223,6 +474,7 @@ const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) => {
{dialogProps.confirmButtonName}
</StyledButton>
</DialogActions>
<ConfirmDialog />
</Dialog>
) : null
@@ -1,186 +0,0 @@
import { createPortal } from 'react-dom'
import PropTypes from 'prop-types'
import { useState, useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'
// Material
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Box, Typography, Stack } from '@mui/material'
// Project imports
import { StyledButton } from '@/ui-component/button/StyledButton'
import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
import { File } from '@/ui-component/file/File'
// Icons
import { IconFileUpload, IconX } from '@tabler/icons-react'
// API
import apikeyAPI from '@/api/apikey'
// utils
import useNotifier from '@/utils/useNotifier'
// const
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions'
import { Dropdown } from '@/ui-component/dropdown/Dropdown'
const importModes = [
{
label: 'Add & Overwrite',
name: 'overwriteIfExist',
description: 'Add keys and overwrite existing keys with the same name'
},
{
label: 'Add & Ignore',
name: 'ignoreIfExist',
description: 'Add keys and ignore existing keys with the same name'
},
{
label: 'Add & Verify',
name: 'errorIfExist',
description: 'Add Keys and throw error if key with same name exists'
},
{
label: 'Replace All',
name: 'replaceAll',
description: 'Replace all keys with the imported keys'
}
]
const UploadJSONFileDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
const portalElement = document.getElementById('portal')
const dispatch = useDispatch()
// ==============================|| Snackbar ||============================== //
useNotifier()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const [selectedFile, setSelectedFile] = useState()
const [importMode, setImportMode] = useState('overwrite')
useEffect(() => {
return () => {
setSelectedFile()
}
}, [dialogProps])
useEffect(() => {
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
else dispatch({ type: HIDE_CANVAS_DIALOG })
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
}, [show, dispatch])
const importKeys = async () => {
try {
const obj = {
importMode: importMode,
jsonFile: selectedFile
}
const createResp = await apikeyAPI.importAPI(obj)
if (createResp.data) {
enqueueSnackbar({
message: 'Imported keys successfully!',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onConfirm(createResp.data.id)
}
} catch (error) {
enqueueSnackbar({
message: `Failed to import keys: ${
typeof error.response.data === 'object' ? error.response.data.message : error.response.data
}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onCancel()
}
}
const component = show ? (
<Dialog
fullWidth
maxWidth='sm'
open={show}
onClose={onCancel}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<IconFileUpload style={{ marginRight: '10px' }} />
Import API Keys
</div>
</DialogTitle>
<DialogContent>
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>
Import api.json file
<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
</Stack>
<File
disabled={false}
fileType='.json'
onChange={(newValue) => setSelectedFile(newValue)}
value={selectedFile ?? 'Choose a file to upload'}
/>
</Box>
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>
Import Mode
<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
</Stack>
<Dropdown
key={importMode}
name={importMode}
options={importModes}
onSelect={(newValue) => setImportMode(newValue)}
value={importMode ?? 'choose an option'}
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => onCancel()}>{dialogProps.cancelButtonName}</Button>
<StyledButton disabled={!selectedFile} variant='contained' onClick={importKeys}>
{dialogProps.confirmButtonName}
</StyledButton>
</DialogActions>
<ConfirmDialog />
</Dialog>
) : null
return createPortal(component, portalElement)
}
UploadJSONFileDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onCancel: PropTypes.func,
onConfirm: PropTypes.func
}
export default UploadJSONFileDialog
+50 -63
View File
@@ -1,14 +1,18 @@
import * as PropTypes from 'prop-types'
import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions'
import moment from 'moment/moment'
import { useEffect, useState } from 'react'
import * as PropTypes from 'prop-types'
import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'
// material-ui
import {
Button,
Box,
Button,
Chip,
Collapse,
IconButton,
Paper,
Popover,
Skeleton,
Stack,
Table,
@@ -16,25 +20,20 @@ import {
TableContainer,
TableHead,
TableRow,
Paper,
IconButton,
Popover,
Collapse,
Typography
} from '@mui/material'
import TableCell, { tableCellClasses } from '@mui/material/TableCell'
import { useTheme, styled } from '@mui/material/styles'
import { styled, useTheme } from '@mui/material/styles'
// project imports
import MainCard from '@/ui-component/cards/MainCard'
import APIKeyDialog from './APIKeyDialog'
import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
import ViewHeader from '@/layout/MainLayout/ViewHeader'
import ErrorBoundary from '@/ErrorBoundary'
import { PermissionButton, StyledPermissionButton } from '@/ui-component/button/RBACButtons'
import { Available } from '@/ui-component/rbac/available'
import UploadJSONFileDialog from '@/views/apikey/UploadJSONFileDialog'
import ViewHeader from '@/layout/MainLayout/ViewHeader'
import { StyledPermissionButton } from '@/ui-component/button/RBACButtons'
import MainCard from '@/ui-component/cards/MainCard'
import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
import { Available } from '@/ui-component/rbac/available'
import APIKeyDialog from './APIKeyDialog'
// API
import apiKeyApi from '@/api/apikey'
@@ -48,19 +47,8 @@ import useConfirm from '@/hooks/useConfirm'
import useNotifier from '@/utils/useNotifier'
// Icons
import {
IconTrash,
IconEdit,
IconCopy,
IconChevronsUp,
IconChevronsDown,
IconX,
IconPlus,
IconEye,
IconEyeOff,
IconFileUpload
} from '@tabler/icons-react'
import APIEmptySVG from '@/assets/images/api_empty.svg'
import { IconChevronsDown, IconChevronsUp, IconCopy, IconEdit, IconEye, IconEyeOff, IconPlus, IconTrash, IconX } from '@tabler/icons-react'
// ==============================|| APIKey ||============================== //
@@ -88,13 +76,15 @@ function APIKeyRow(props) {
const [open, setOpen] = useState(false)
const theme = useTheme()
const permissions = props.apiKey.permissions || []
return (
<>
<TableRow sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<StyledTableCell scope='row' style={{ width: '15%' }}>
{props.apiKey.keyName}
</StyledTableCell>
<StyledTableCell style={{ width: '40%' }}>
<StyledTableCell style={{ width: '25%' }}>
{props.showApiKeys.includes(props.apiKey.apiKey)
? props.apiKey.apiKey
: `${props.apiKey.apiKey.substring(0, 2)}${'•'.repeat(18)}${props.apiKey.apiKey.substring(
@@ -124,6 +114,29 @@ function APIKeyRow(props) {
</Typography>
</Popover>
</StyledTableCell>
<StyledTableCell sx={{ width: '25%' }}>
<Stack sx={{ flexDirection: 'row' }}>
<Typography
variant='subtitle2'
color='textPrimary'
sx={{
width: '100%',
overflow: 'hidden',
textOverflow: 'ellipsis',
display: '-webkit-box',
WebkitLineClamp: '2',
WebkitBoxOrient: 'vertical'
}}
>
{permissions.map((d, key) => (
<React.Fragment key={key}>
{d}
{', '}
</React.Fragment>
))}
</Typography>
</Stack>
</StyledTableCell>
<StyledTableCell>
{props.apiKey.chatFlows.length}{' '}
{props.apiKey.chatFlows.length > 0 && (
@@ -150,7 +163,7 @@ function APIKeyRow(props) {
</TableRow>
{open && (
<TableRow sx={{ '& td': { border: 0 } }}>
<StyledTableCell sx={{ p: 2 }} colSpan={6}>
<StyledTableCell sx={{ p: 2 }} colSpan={7}>
<Collapse in={open} timeout='auto' unmountOnExit>
<Box sx={{ borderRadius: 2, border: 1, borderColor: theme.palette.grey[900] + 25, overflow: 'hidden' }}>
<Table aria-label='chatflow table'>
@@ -219,9 +232,6 @@ const APIKey = () => {
const [showApiKeys, setShowApiKeys] = useState([])
const openPopOver = Boolean(anchorEl)
const [showUploadDialog, setShowUploadDialog] = useState(false)
const [uploadDialogProps, setUploadDialogProps] = useState({})
const [search, setSearch] = useState('')
/* Table Pagination */
@@ -296,17 +306,6 @@ const APIKey = () => {
setShowDialog(true)
}
const uploadDialog = () => {
const dialogProp = {
type: 'ADD',
cancelButtonName: 'Cancel',
confirmButtonName: 'Upload',
data: {}
}
setUploadDialogProps(dialogProp)
setShowUploadDialog(true)
}
const deleteKey = async (key) => {
const confirmPayload = {
title: `Delete`,
@@ -361,7 +360,6 @@ const APIKey = () => {
const onConfirm = () => {
setShowDialog(false)
setShowUploadDialog(false)
refresh(currentPage, pageLimit)
}
@@ -395,16 +393,6 @@ const APIKey = () => {
title='API Keys'
description='Flowise API & SDK authentication keys'
>
<PermissionButton
permissionId={'apikeys:import'}
variant='outlined'
sx={{ borderRadius: 2, height: '100%' }}
onClick={uploadDialog}
startIcon={<IconFileUpload />}
id='btn_importApiKeys'
>
Import
</PermissionButton>
<StyledPermissionButton
permissionId={'apikeys:create'}
variant='contained'
@@ -445,6 +433,7 @@ const APIKey = () => {
<TableRow>
<StyledTableCell>Key Name</StyledTableCell>
<StyledTableCell>API Key</StyledTableCell>
<StyledTableCell>Permissions</StyledTableCell>
<StyledTableCell>Usage</StyledTableCell>
<StyledTableCell>Updated</StyledTableCell>
<Available permission={'apikeys:update,apikeys:create'}>
@@ -471,6 +460,9 @@ const APIKey = () => {
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<Available permission={'apikeys:update,apikeys:create'}>
<StyledTableCell> </StyledTableCell>
</Available>
@@ -491,6 +483,9 @@ const APIKey = () => {
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<Available permission={'apikeys:update,apikeys:create'}>
<StyledTableCell> </StyledTableCell>
</Available>
@@ -541,14 +536,6 @@ const APIKey = () => {
onConfirm={onConfirm}
setError={setError}
></APIKeyDialog>
{showUploadDialog && (
<UploadJSONFileDialog
show={showUploadDialog}
dialogProps={uploadDialogProps}
onCancel={() => setShowUploadDialog(false)}
onConfirm={onConfirm}
></UploadJSONFileDialog>
)}
<ConfirmDialog />
</>
)
@@ -1,18 +1,18 @@
import { createPortal } from 'react-dom'
import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions'
import PropTypes from 'prop-types'
import { useState, useEffect } from 'react'
import { useEffect, useState } from 'react'
import { createPortal } from 'react-dom'
import { useDispatch, useSelector } from 'react-redux'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'
// Material
import { Box, Typography, OutlinedInput, Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material'
import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, OutlinedInput, Typography } from '@mui/material'
// Project imports
import { StyledButton } from '@/ui-component/button/StyledButton'
import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
// Icons
import { IconX, IconUser } from '@tabler/icons-react'
import { IconUser, IconX } from '@tabler/icons-react'
// API
import authApi from '@/api/auth'
@@ -34,7 +34,7 @@ const CreateEditRoleDialog = ({ show, dialogProps, onCancel, onConfirm, setError
const portalElement = document.getElementById('portal')
const dispatch = useDispatch()
const { isEnterpriseLicensed } = useConfig()
const { isOpenSource, isEnterpriseLicensed, isCloud } = useConfig()
// ==============================|| Snackbar ||============================== //
@@ -131,7 +131,7 @@ const CreateEditRoleDialog = ({ show, dialogProps, onCancel, onConfirm, setError
if ((dialogProps.type === 'EDIT' || dialogProps.type === 'VIEW') && dialogProps.data) {
setDialogData(dialogProps.data)
}
getAllPermissionsApi.request()
getAllPermissionsApi.request('ROLE')
return () => {
setRoleName('')
setRoleDescription('')
@@ -158,18 +158,20 @@ const CreateEditRoleDialog = ({ show, dialogProps, onCancel, onConfirm, setError
setRoleName(dialogData.name)
setRoleDescription(dialogData.description)
const permissions = getAllPermissionsApi.data
// Filter out enterprise permissions if not licensed
if (!isEnterpriseLicensed) {
Object.keys(permissions).forEach((category) => {
permissions[category] = permissions[category].filter((permission) => !permission.isEnterprise)
Object.keys(permissions).forEach((category) => {
permissions[category] = permissions[category].filter((permission) => {
if (isOpenSource) return permission.isOpenSource
if (isEnterpriseLicensed) return permission.isEnterprise
if (isCloud) return permission.isCloud
return false // fallback - show nothing if no platform is set
})
// Remove categories that have no permissions left
Object.keys(permissions).forEach((category) => {
if (permissions[category].length === 0) {
delete permissions[category]
}
})
}
})
// Remove categories that have no permissions left
Object.keys(permissions).forEach((category) => {
if (permissions[category].length === 0) {
delete permissions[category]
}
})
setPermissions(permissions)
if ((dialogProps.type === 'EDIT' || dialogProps.type === 'VIEW') && dialogProps.data) {
const dialogDataPermissions = JSON.parse(dialogData.permissions)
+1 -1
View File
@@ -81,7 +81,7 @@ function ViewPermissionsDrawer(props) {
useEffect(() => {
if (props.open) {
getAllPermissionsApi.request()
getAllPermissionsApi.request('ROLE')
}
return () => {
setSelectedPermissions({})