diff --git a/app/components/editor/CustomConfigPanel.tsx b/app/components/editor/CustomConfigPanel.tsx new file mode 100644 index 0000000..3d7171e --- /dev/null +++ b/app/components/editor/CustomConfigPanel.tsx @@ -0,0 +1,341 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { clsx } from 'clsx'; +import CodeMirror from '@uiw/react-codemirror'; +import { monokai } from '@uiw/codemirror-theme-monokai'; +import { basicSetup } from 'codemirror'; +import { indentUnit } from '@codemirror/language'; +import { StreamLanguage } from '@codemirror/language'; +import { go } from '@codemirror/legacy-modes/mode/go'; +import { EditorView } from '@codemirror/view'; + +interface FunctionConfig { + id: string; + name: string; + body: string; +} + +interface CustomConfigPanelProps { + open: boolean; + setOpen: (value: boolean) => void; + showCustomConfig: boolean; + customConfig: string; + setCustomConfigPersistent: (value: string) => void; + textClass: string; + t: (key: string) => string; +} + +export const CustomConfigPanel: React.FC = ({ + open, + setOpen, + showCustomConfig, + customConfig, + setCustomConfigPersistent, + textClass, + t, +}) => { + const [functions, setFunctions] = useState([]); + const [isEditing, setIsEditing] = useState(false); + const previousConfig = useRef(customConfig); + + const parseConfig = (configStr: string) => { + try { + const config = eval(configStr) as { + functions?: Record; + matchingForGFunction?: Function | string; + matchingDomainForGFunction?: Function | string; + }; + + const newFunctions: FunctionConfig[] = []; + + if (config?.functions) { + Object.entries(config.functions).forEach(([name, body]) => { + newFunctions.push({ + id: `${Date.now()}-${Math.random()}`, + name, + body: body.toString(), + }); + }); + } + + ['matchingForGFunction', 'matchingDomainForGFunction'].forEach((fnName) => { + if (config?.[fnName as keyof typeof config]) { + newFunctions.push({ + id: `${Date.now()}-${Math.random()}`, + name: fnName, + body: config[fnName as keyof typeof config]!.toString(), + }); + } + }); + + return newFunctions; + } catch (error) { + return null; + } + }; + + useEffect(() => { + if (!isEditing && customConfig !== previousConfig.current) { + const parsedFunctions = parseConfig(customConfig); + if (parsedFunctions) { + setFunctions(parsedFunctions); + previousConfig.current = customConfig; + } + } + }, [customConfig, isEditing]); + + // Add new function + const addNewFunction = () => { + const regularFunctionCount = functions.filter((f) => { + return !['matchingForGFunction', 'matchingDomainForGFunction'].includes(f.name); + }).length; + + const newFunction = { + id: Date.now().toString(), + name: `my_func${regularFunctionCount + 1}`, + body: '(arg1, arg2) => {\n return arg1.endsWith(arg2);\n}', + }; + setFunctions([...functions, newFunction]); + updateCustomConfig([...functions, newFunction]); + }; + + // Delete function + const deleteFunction = (id: string) => { + const updatedFunctions = functions.filter((f) => { + return f.id !== id; + }); + setFunctions(updatedFunctions); + updateCustomConfig(updatedFunctions); + }; + + // Update function content + const updateFunction = (id: string, field: keyof FunctionConfig, value: string) => { + const updatedFunctions = functions.map((f) => { + if (f.id === id) { + return { ...f, [field]: value }; + } + return f; + }); + setFunctions(updatedFunctions); + updateCustomConfig(updatedFunctions); + }; + + // Add new matching function + const addMatchingFunction = () => { + if ( + functions.some((f) => { + return f.name === 'matchingForGFunction'; + }) + ) { + return; + } + + const template = { + id: Date.now().toString(), + name: 'matchingForGFunction', + body: `(user, role) => { + return user.department === role.department; +}`, + }; + setFunctions([...functions, template]); + updateCustomConfig([...functions, template]); + }; + + // Add new matching domain function + const addMatchingDomainFunction = () => { + if ( + functions.some((f) => { + return f.name === 'matchingDomainForGFunction'; + }) + ) { + return; + } + + const template = { + id: Date.now().toString(), + name: 'matchingDomainForGFunction', + body: `(domain1, domain2) => { + return domain1.startsWith(domain2); +}`, + }; + setFunctions([...functions, template]); + updateCustomConfig([...functions, template]); + }; + + // Generate a complete configuration string. + const updateCustomConfig = (updatedFunctions: FunctionConfig[]) => { + setIsEditing(true); + + const regularFunctions = updatedFunctions.filter((f) => { + return !['matchingForGFunction', 'matchingDomainForGFunction'].includes(f.name); + }); + + const specialFunctions = { + matchingForGFunction: + updatedFunctions.find((f) => { + return f.name === 'matchingForGFunction'; + })?.body || 'undefined', + matchingDomainForGFunction: + updatedFunctions.find((f) => { + return f.name === 'matchingDomainForGFunction'; + })?.body || 'undefined', + }; + + const functionsString = regularFunctions + .map((f) => { + return `${f.name}: ${f.body}`; + }) + .join(',\n '); + + const configString = `(function() { + return { + functions: { + ${functionsString} + }, + matchingForGFunction: ${specialFunctions.matchingForGFunction}, + matchingDomainForGFunction: ${specialFunctions.matchingDomainForGFunction} + }; + })();`; + + setCustomConfigPersistent(configString); + previousConfig.current = configString; + + setTimeout(() => { + return setIsEditing(false); + }, 0); + }; + + const hasMatchingFunction = (name: string) => { + return functions.some((f) => { + return f.name === name; + }); + }; + + return ( + <> + + + {(showCustomConfig || open) && ( +
+
+
{t('Custom Functions')}
+
+ +
+ {functions.map((func) => { + return ( +
+
+ { + return updateFunction(func.id, 'name', e.target.value); + }} + className="px-2 py-1 border rounded w-64" + placeholder={t('Function name')} + disabled={func.name === 'matchingForGFunction' || func.name === 'matchingDomainForGFunction'} + /> + +
+ +
+ { + return updateFunction(func.id, 'body', value); + }} + basicSetup={{ + lineNumbers: true, + highlightActiveLine: true, + bracketMatching: true, + indentOnInput: true, + }} + extensions={[basicSetup, StreamLanguage.define(go), indentUnit.of(' '), EditorView.lineWrapping]} + className="h-full" + /> +
+
+ ); + })} +
+ +
+ + + + + +
+
+ )} + + ); +}; diff --git a/app/components/editor/casbin-mode/example.ts b/app/components/editor/casbin-mode/example.ts index 1e739c2..d445589 100644 --- a/app/components/editor/casbin-mode/example.ts +++ b/app/components/editor/casbin-mode/example.ts @@ -432,19 +432,17 @@ g, bob, data2_allow_group`, export const defaultCustomConfig = `(function() { return { - /** - * Here is custom functions for Casbin. - * Currently, there are built-in globMatch, keyMatch, keyMatch2, keyMatch3, keyMatch4, regexMatch, ipMatch. - */ - functions: {}, - /** - * If the value is undefined, the Casbin does not use it. - * example: - * matchingForGFunction: 'globMatch' - * matchingDomainForGFunction: 'keyMatch' - */ - matchingForGFunction: undefined, - matchingDomainForGFunction: undefined + functions: { + my_func1: (arg1, arg2) => { + return arg1.endsWith(arg2); +} + }, + matchingForGFunction: (user, role) => { + return user.department === role.department; +}, + matchingDomainForGFunction: (domain1, domain2) => { + return domain1.startsWith(domain2); +} }; })();`; export const defaultEnforceContext = `{ diff --git a/app/components/editor/hooks/useRunTest.tsx b/app/components/editor/hooks/useRunTest.tsx index 1a9bab3..27227e2 100755 --- a/app/components/editor/hooks/useRunTest.tsx +++ b/app/components/editor/hooks/useRunTest.tsx @@ -89,6 +89,12 @@ async function enforcer(props: RunTestProps) { try { const e = await newEnforcer(newModel(props.model), props.policy ? new StringAdapter(props.policy) : undefined); + if (!e.getRoleManager()) { + // Create a new RoleManager instance, 10 is the maximum role level + const roleManager = new DefaultRoleManager(10); + e.setRoleManager(roleManager); + } + const customConfigCode = props.customConfig; if (customConfigCode) { try { diff --git a/app/components/editor/index.tsx b/app/components/editor/index.tsx index cdc3064..f46741b 100755 --- a/app/components/editor/index.tsx +++ b/app/components/editor/index.tsx @@ -6,8 +6,7 @@ import { clsx } from 'clsx'; import CodeMirror from '@uiw/react-codemirror'; import { monokai } from '@uiw/codemirror-theme-monokai'; import { basicSetup } from 'codemirror'; -import { indentUnit, StreamLanguage } from '@codemirror/language'; -import { go } from '@codemirror/legacy-modes/mode/go'; +import { indentUnit } from '@codemirror/language'; import { EditorView } from '@codemirror/view'; import { CasbinConfSupport } from '@/app/components/editor/casbin-mode/casbin-conf'; import { CasbinPolicySupport } from '@/app/components/editor/casbin-mode/casbin-csv'; @@ -25,14 +24,30 @@ import LanguageMenu from '@/app/components/LanguageMenu'; import { linter, lintGutter } from '@codemirror/lint'; import { casbinLinter } from '@/app/utils/casbinLinter'; import { toast, Toaster } from 'react-hot-toast'; +import { CustomConfigPanel } from './CustomConfigPanel'; export const EditorScreen = () => { const { - modelKind, setModelKind, modelText, setModelText, policy, setPolicy, request, - setRequest, echo, setEcho, requestResult, setRequestResult, customConfig, setCustomConfig, share, setShare, - enforceContextData, setEnforceContextData, setPolicyPersistent, setModelTextPersistent, - setCustomConfigPersistent, setRequestPersistent, setEnforceContextDataPersistent, handleShare, - } = useIndex(); + modelKind, + setModelKind, + modelText, + policy, + request, + echo, + setEcho, + requestResult, + setRequestResult, + customConfig, + share, + setShare, + enforceContextData, + setPolicyPersistent, + setModelTextPersistent, + setCustomConfigPersistent, + setRequestPersistent, + setEnforceContextDataPersistent, + handleShare, + } = useIndex(); const [open, setOpen] = useState(true); const { enforcer } = useRunTest(); const { shareInfo } = useShareInfo(); @@ -98,61 +113,23 @@ export const EditorScreen = () => {
- - -
- {(showCustomConfig || open) &&
{t('Custom config')}
} -
-
- {(showCustomConfig || open) && ( -
- -
- )} -
+
@@ -421,9 +398,7 @@ export const EditorScreen = () => { } else if (Array.isArray(v)) { const formattedResults = v.map((res) => { if (typeof res === 'object') { - const reasonString = Array.isArray(res.reason) && res.reason.length > 0 - ? ` Reason: ${JSON.stringify(res.reason)}` - : ''; + const reasonString = Array.isArray(res.reason) && res.reason.length > 0 ? ` Reason: ${JSON.stringify(res.reason)}` : ''; return `${res.okEx}${reasonString}`; } return res; diff --git a/app/globals.css b/app/globals.css index 56c0343..a264cea 100644 --- a/app/globals.css +++ b/app/globals.css @@ -14,3 +14,30 @@ button:hover img { max-height: var(--radix-dropdown-menu-content-available-height); overflow-y: auto; } + +/* ------------------------------ */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.2); + border-radius: 3px; +} + +::-webkit-scrollbar-track { + background: transparent; +} +/* ------------------------------ */ + + +/* ------------------------------ */ +.btn-active { + @apply border-[#453d7d] text-[#453d7a] bg-[#efefef] hover:bg-[#453d7d] hover:text-white; +} + +.btn-disabled { + @apply border-gray-300 text-gray-300 bg-gray-100 cursor-not-allowed; +} +/* ------------------------------ */ \ No newline at end of file diff --git a/app/utils/contentExtractor.ts b/app/utils/contentExtractor.ts index 3555284..377bb11 100644 --- a/app/utils/contentExtractor.ts +++ b/app/utils/contentExtractor.ts @@ -8,13 +8,13 @@ const cleanContent = (content: string) => { export const extractPageContent = (boxType: string, t: (key: string) => string, lang: string) => { const mainContent = document.querySelector('main')?.innerText || 'No main content found'; - const customConfigMatch = mainContent.match(new RegExp(`${t('Custom config')}\\s+([\\s\\S]*?)\\s+${t('Model')}`)); + const customConfigMatch = mainContent.match(new RegExp(`${t('Custom Functions')}\\s+([\\s\\S]*?)\\s+${t('Model')}`)); const modelMatch = mainContent.match(new RegExp(`${t('Model')}\\s+([\\s\\S]*?)\\s+${t('Policy')}`)); const policyMatch = mainContent.match(new RegExp(`${t('Policy')}\\s+([\\s\\S]*?)\\s+${t('Request')}`)); const requestMatch = mainContent.match(new RegExp(`${t('Request')}\\s+([\\s\\S]*?)\\s+${t('Enforcement Result')}`)); const enforcementResultMatch = mainContent.match(new RegExp(`${t('Enforcement Result')}\\s+([\\s\\S]*?)\\s+${t('RUN THE TEST')}`)); - const customConfig = customConfigMatch ? cleanContent(customConfigMatch[1]) : 'No custom config found'; + const customConfig = customConfigMatch ? cleanContent(customConfigMatch[1]) : 'No Custom Functions found'; const model = modelMatch ? cleanContent(modelMatch[1].replace(new RegExp(`${t('Select your model')}[\\s\\S]*?${t('RESET')}`, 'i'), '')) : 'No model found'; @@ -33,7 +33,7 @@ export const extractPageContent = (boxType: string, t: (key: string) => string, .join('\n'); }; const extractedContent = removeEmptyLines(` - Custom config: ${cleanContent(customConfig)} + Custom Functions: ${cleanContent(customConfig)} Model: ${cleanContent(model)} Policy: ${cleanContent(policy)} Request: ${cleanContent(request)} diff --git a/messages/ar.json b/messages/ar.json index 3bb3225..3d83a0f 100644 --- a/messages/ar.json +++ b/messages/ar.json @@ -1,5 +1,8 @@ { - "Custom config": "تكوين مخصص", + "Custom Functions": "وظائف مخصصة", + "Add Function": "إضافة وظيفة", + "Add Role Matching": "إضافة مطابقة الدور", + "Add Domain Matching": "إضافة مطابقة المجال", "Model": "نموذج", "Select your model": "حدد النموذج الخاص بك", "RESET": "إعادة ضبط", diff --git a/messages/de.json b/messages/de.json index b2e3a5c..e2c77cf 100644 --- a/messages/de.json +++ b/messages/de.json @@ -1,5 +1,8 @@ { - "Custom config": "Benutzerdefinierte Konfiguration", + "Custom Functions": "Benutzerdefinierte Funktionen", + "Add Function": "Funktion hinzufügen", + "Add Role Matching": "Rolle hinzufügen", + "Add Domain Matching": "Domain hinzufügen", "Model": "Modell", "Select your model": "Wählen Sie Ihr Modell", "RESET": "Zurücksetzen", diff --git a/messages/en.json b/messages/en.json index c7d101e..8c41de2 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1,5 +1,8 @@ { - "Custom config": "Custom config", + "Custom Functions": "Custom Functions", + "Add Function": "Add Function", + "Add Role Matching": "Add Role Matching", + "Add Domain Matching": "Add Domain Matching", "Model": "Model", "Select your model": "Select your model", "RESET": "RESET", diff --git a/messages/es.json b/messages/es.json index 2d760ac..e17a787 100644 --- a/messages/es.json +++ b/messages/es.json @@ -1,5 +1,8 @@ { - "Custom config": "Configuración personalizada", + "Custom Functions": "Funciones personalizadas", + "Add Function": "Añadir función", + "Add Role Matching": "Añadir coincidencia de rol", + "Add Domain Matching": "Añadir coincidencia de dominio", "Model": "Modelo", "Select your model": "Seleccione su modelo", "RESET": "REINICIAR", diff --git a/messages/fr.json b/messages/fr.json index 0b10ac8..65ab2ae 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -1,5 +1,8 @@ { - "Custom config": "Configuration personnalisée", + "Custom Functions": "Fonctions personnalisées", + "Add Function": "Ajouter une fonction", + "Add Role Matching": "Ajouter une correspondance de rôle", + "Add Domain Matching": "Ajouter une correspondance de domaine", "Model": "Modèle", "Select your model": "Sélectionnez votre modèle", "RESET": "Réinitialiser", diff --git a/messages/id.json b/messages/id.json index 0aa1557..765d38a 100644 --- a/messages/id.json +++ b/messages/id.json @@ -1,5 +1,8 @@ { - "Custom config": "Konfigurasi khusus", + "Custom Functions": "Fungsi khusus", + "Add Function": "Tambah fungsi", + "Add Role Matching": "Tambah penyokong peran", + "Add Domain Matching": "Tambah penyokong domain", "Model": "Model", "Select your model": "Pilih model Anda", "RESET": "RESET", diff --git a/messages/it.json b/messages/it.json index 5d22da6..3538ccc 100644 --- a/messages/it.json +++ b/messages/it.json @@ -1,5 +1,8 @@ { - "Custom config": "Configurazione personalizzata", + "Custom Functions": "Funzioni personalizzate", + "Add Function": "Aggiungi funzione", + "Add Role Matching": "Aggiungi corrispondenza di ruolo", + "Add Domain Matching": "Aggiungi corrispondenza di dominio", "Model": "Modello", "Select your model": "Seleziona il tuo modello", "RESET": "REIMPOSTA", diff --git a/messages/ja.json b/messages/ja.json index d99eeb1..7bcc7ed 100644 --- a/messages/ja.json +++ b/messages/ja.json @@ -1,5 +1,8 @@ { - "Custom config": "カスタム設定", + "Custom Functions": "カスタム関数", + "Add Function": "関数を追加", + "Add Role Matching": "ロールマッチングを追加", + "Add Domain Matching": "ドメインマッチングを追加", "Model": "モデル", "Select your model": "モデルを選択", "RESET": "リセット", diff --git a/messages/ko.json b/messages/ko.json index 2e358ed..5d3bac0 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -1,5 +1,8 @@ { - "Custom config": "사용자 정의 구성", + "Custom Functions": "사용자 정의 함수", + "Add Function": "함수 추가", + "Add Role Matching": "역할 매칭 추가", + "Add Domain Matching": "도메인 매칭 추가", "Model": "모델", "Select your model": "모델 선택", "RESET": "재설정", diff --git a/messages/ms.json b/messages/ms.json index 2a4c8b9..53ca679 100644 --- a/messages/ms.json +++ b/messages/ms.json @@ -1,5 +1,8 @@ { - "Custom config": "Konfigurasi khusus", + "Custom Functions": "Fungsi khusus", + "Add Function": "Tambah fungsi", + "Add Role Matching": "Tambah penyokong peran", + "Add Domain Matching": "Tambah penyokong domain", "Model": "Model", "Select your model": "Pilih model anda", "RESET": "TETAPKAN SEMULA", diff --git a/messages/pt.json b/messages/pt.json index 2dca6e6..6873b8e 100644 --- a/messages/pt.json +++ b/messages/pt.json @@ -1,5 +1,8 @@ { - "Custom config": "Configuração personalizada", + "Custom Functions": "Funções personalizadas", + "Add Function": "Adicionar função", + "Add Role Matching": "Adicionar correspondência de função", + "Add Domain Matching": "Adicionar correspondência de domínio", "Model": "Modelo", "Select your model": "Selecione seu modelo", "RESET": "REINICIAR", diff --git a/messages/ru.json b/messages/ru.json index 3deae5e..f2e32c5 100644 --- a/messages/ru.json +++ b/messages/ru.json @@ -1,5 +1,8 @@ { - "Custom config": "Пользовательская конфигурация", + "Custom Functions": "Пользовательские функции", + "Add Function": "Добавить функцию", + "Add Role Matching": "Добавить соответствие ролей", + "Add Domain Matching": "Добавить соответствие доменов", "Model": "Модель", "Select your model": "Выберите вашу модель", "RESET": "СБРОС", diff --git a/messages/tr.json b/messages/tr.json index d982d2b..08e9c21 100644 --- a/messages/tr.json +++ b/messages/tr.json @@ -1,5 +1,8 @@ { - "Custom config": "Özel yapılandırma", + "Custom Functions": "Özel yapılandırma", + "Add Function": "Fonksiyon ekle", + "Add Role Matching": "Rol eşleştirme ekle", + "Add Domain Matching": "Domain eşleştirme ekle", "Model": "Model", "Select your model": "Modelinizi seçin", "RESET": "SIFIRLA", diff --git a/messages/vi.json b/messages/vi.json index f59241d..8e306ac 100644 --- a/messages/vi.json +++ b/messages/vi.json @@ -1,5 +1,8 @@ { - "Custom config": "Cấu hình tùy chỉnh", + "Custom Functions": "Cấu hình tùy chỉnh", + "Add Function": "Thêm hàm", + "Add Role Matching": "Thêm phù hợp với vai trò", + "Add Domain Matching": "Thêm phù hợp với miền", "Model": "Mô hình", "Select your model": "Chọn mô hình của bạn", "RESET": "CÀI LẠI", diff --git a/messages/zh-Hant.json b/messages/zh-Hant.json index 444f226..6904531 100644 --- a/messages/zh-Hant.json +++ b/messages/zh-Hant.json @@ -1,5 +1,8 @@ { - "Custom config": "自定義配置", + "Custom Functions": "自定義函數", + "Add Function": "添加函數", + "Add Role Matching": "添加角色匹配", + "Add Domain Matching": "添加域名匹配", "Model": "模型", "Select your model": "選擇模型", "RESET": "重置", diff --git a/messages/zh.json b/messages/zh.json index 6775eaa..c7fdabe 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -1,5 +1,8 @@ { - "Custom config": "自定义配置", + "Custom Functions": "自定义函数", + "Add Function": "添加函数", + "Add Role Matching": "添加角色匹配", + "Add Domain Matching": "添加域名匹配", "Model": "模型", "Select your model": "选择模型", "RESET": "重置",