mirror of
https://github.com/FlowiseAI/Flowise.git
synced 2026-10-11 21:59:56 +00:00
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:
co-authored by
gemini-code-assist[bot]
Copilot Autofix powered by AI
Jocelyn Lin
parent
d848baeb6b
commit
e135b29433
@@ -1,7 +1,5 @@
|
||||
PORT=3000
|
||||
|
||||
# APIKEY_PATH=/your_apikey_path/.flowise # (will be deprecated by end of 2025)
|
||||
|
||||
############################################################################################################
|
||||
############################################## DATABASE ####################################################
|
||||
############################################################################################################
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
WORKER_PORT=5566
|
||||
|
||||
# APIKEY_PATH=/your_apikey_path/.flowise # (will be deprecated by end of 2025)
|
||||
|
||||
############################################################################################################
|
||||
############################################## DATABASE ####################################################
|
||||
############################################################################################################
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
PORT=3000
|
||||
|
||||
# APIKEY_PATH=/your_apikey_path/.flowise # (will be deprecated by end of 2025)
|
||||
|
||||
############################################################################################################
|
||||
############################################## DATABASE ####################################################
|
||||
############################################################################################################
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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' }}>* </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' }}>* </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' }}> *</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' }}> *</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
|
||||
@@ -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)
|
||||
|
||||
@@ -81,7 +81,7 @@ function ViewPermissionsDrawer(props) {
|
||||
|
||||
useEffect(() => {
|
||||
if (props.open) {
|
||||
getAllPermissionsApi.request()
|
||||
getAllPermissionsApi.request('ROLE')
|
||||
}
|
||||
return () => {
|
||||
setSelectedPermissions({})
|
||||
|
||||
Reference in New Issue
Block a user