From df5883165b3a50ab742509a8dc266a898995e1f6 Mon Sep 17 00:00:00 2001 From: Coki Date: Tue, 3 Dec 2024 16:15:32 +0800 Subject: [PATCH 1/7] feat: add config templates and integrate into editor component --- app/components/editor/casbin-mode/example.ts | 17 -- app/components/editor/hooks/useIndex.tsx | 7 +- app/components/editor/index.tsx | 33 ++- app/constants/configTemplates.ts | 211 +++++++++++++++++++ 4 files changed, 242 insertions(+), 26 deletions(-) create mode 100644 app/constants/configTemplates.ts diff --git a/app/components/editor/casbin-mode/example.ts b/app/components/editor/casbin-mode/example.ts index 1e739c2..cdd6eed 100644 --- a/app/components/editor/casbin-mode/example.ts +++ b/app/components/editor/casbin-mode/example.ts @@ -430,23 +430,6 @@ 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 - }; -})();`; export const defaultEnforceContext = `{ "r": "r", "p": "p", diff --git a/app/components/editor/hooks/useIndex.tsx b/app/components/editor/hooks/useIndex.tsx index 4a8e2c5..6247c4b 100644 --- a/app/components/editor/hooks/useIndex.tsx +++ b/app/components/editor/hooks/useIndex.tsx @@ -1,7 +1,8 @@ import React, { isValidElement, ReactNode, useEffect, useRef, useState } from 'react'; -import { defaultCustomConfig, defaultEnforceContext, example, ModelKind } from '@/app/components/editor/casbin-mode/example'; +import { defaultEnforceContext, example, ModelKind } from '@/app/components/editor/casbin-mode/example'; import { ShareFormat } from '@/app/components/editor/hooks/useShareInfo'; import { defaultEnforceContextData } from '@/app/components/editor/hooks/useSetupEnforceContext'; +import { CONFIG_TEMPLATES } from '@/app/constants/configTemplates'; export default function useIndex() { const [modelKind, setModelKind] = useState('basic'); @@ -10,7 +11,7 @@ export default function useIndex() { const [request, setRequest] = useState(''); const [echo, setEcho] = useState(<>); const [requestResult, setRequestResult] = useState(''); - const [customConfig, setCustomConfig] = useState(''); + const [customConfig, setCustomConfig] = useState(CONFIG_TEMPLATES.default.value); const [share, setShare] = useState(''); const [triggerUpdate, setTriggerUpdate] = useState(0); const [enforceContextData, setEnforceContextData] = useState(new Map(defaultEnforceContextData)); @@ -72,7 +73,7 @@ export default function useIndex() { setPolicy(shared?.policy ?? example[modelKind].policy); setModelText(shared?.model ?? example[modelKind].model); setRequest(shared?.request ?? example[modelKind].request); - setCustomConfig(shared?.customConfig ?? defaultCustomConfig); + setCustomConfig(shared?.customConfig ?? CONFIG_TEMPLATES.default.value); setEnforceContextData(new Map(Object.entries(JSON.parse(shared?.enforceContext || example[modelKind].enforceContext || defaultEnforceContext)))); loadState.current.content = undefined; }, [modelKind, triggerUpdate]); diff --git a/app/components/editor/index.tsx b/app/components/editor/index.tsx index cdc3064..7e555ed 100755 --- a/app/components/editor/index.tsx +++ b/app/components/editor/index.tsx @@ -25,6 +25,7 @@ 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 { CONFIG_TEMPLATES } from '@/app/constants/configTemplates'; export const EditorScreen = () => { const { @@ -94,11 +95,18 @@ export const EditorScreen = () => { }, [modelKind, modelText, policy, customConfig, request, enforceContextData, enforcer, setEcho, setRequestResult]); const textClass = clsx(theme === 'dark' ? 'text-gray-200' : 'text-gray-800'); + const handleTemplateChange = (e: React.ChangeEvent) => { + const template = CONFIG_TEMPLATES[e.target.value]; + if (template) { + setCustomConfigPersistent(template.value); + } + }; + return (
{ -
- {(showCustomConfig || open) &&
{t('Custom config')}
} +
+ {(showCustomConfig || open) && ( + <> + + + )}
{(showCustomConfig || open) && ( @@ -421,9 +444,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/constants/configTemplates.ts b/app/constants/configTemplates.ts new file mode 100644 index 0000000..1e56668 --- /dev/null +++ b/app/constants/configTemplates.ts @@ -0,0 +1,211 @@ +/** + * Casbin built-in matching function configuration templates + * + * All key matching functions follow this format: + * bool function_name(string url, string pattern) + * Returns a boolean indicating whether the URL matches the pattern. + */ + +export const CONFIG_TEMPLATES = { + default: { + label: 'default', + value: `(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 + }; +})();` + }, + + keyMatch: { + label: 'RESTful (KeyMatch)', + value: `(function() { + return { + functions: { + /** + * keyMatch supports * wildcard matching + * Example: + * - URL: /alice_data/resource1 + * - Pattern: /alice_data/* + * - Match result: true + * + * Use case: Simple wildcard URL matching + */ + keyMatch: true, + regexMatch: true + }, + matchingForGFunction: undefined, + matchingDomainForGFunction: undefined + }; +})();` + }, + + keyMatch2: { + label: 'RESTful (KeyMatch2)', + value: `(function() { + return { + functions: { + /** + * keyMatch2 supports :param parameter matching + * Example: + * - URL: /alice_data/resource1 + * - Pattern: /alice_data/:resource + * - Match result: true + * + * Use case: Parameter matching in RESTful APIs + */ + keyMatch2: true, + regexMatch: true + }, + matchingForGFunction: undefined, + matchingDomainForGFunction: undefined + }; +})();` + }, + + keyMatch3: { + label: 'RESTful (KeyMatch3)', + value: `(function() { + return { + functions: { + /** + * keyMatch3 supports {param} parameter matching + * Example: + * - URL: /alice_data/resource1 + * - Pattern: /alice_data/{resource} + * - Match result: true + * + * Use case: Parameter matching using curly braces + */ + keyMatch3: true, + regexMatch: true + }, + matchingForGFunction: undefined, + matchingDomainForGFunction: undefined + }; +})();` + }, + + keyMatch4: { + label: 'RESTful (KeyMatch4)', + value: `(function() { + return { + functions: { + /** + * keyMatch4 supports multiple identical parameter matching + * Example: + * - URL: /alice_data/123/book/123 + * - Pattern: /alice_data/{id}/book/{id} + * - Match result: true + * + * Use case: When multiple parameters in a URL need to have the same value + */ + keyMatch4: true, + regexMatch: true + }, + matchingForGFunction: undefined, + matchingDomainForGFunction: undefined + }; +})();` + }, + + keyMatch5: { + label: 'RESTful (KeyMatch5)', + value: `(function() { + return { + functions: { + /** + * keyMatch5 supports query parameter matching + * Example: + * - URL: /alice_data/123/?status=1 + * - Pattern: /alice_data/{id}/* + * - Match result: true + * + * Use case: Handling URL query parameters + */ + keyMatch5: true, + regexMatch: true + }, + matchingForGFunction: undefined, + matchingDomainForGFunction: undefined + }; +})();` + }, + + regexMatch: { + label: 'Regex Match', + value: `(function() { + return { + functions: { + /** + * regexMatch supports regular expression matching + * Example: + * - String: /alice_data/resource1 + * - Regex pattern: ^/alice_data/.*$ + * - Match result: true + * + * Use case: Complex string matching rules + */ + regexMatch: true + }, + matchingForGFunction: undefined, + matchingDomainForGFunction: undefined + }; +})();` + }, + + ipMatch: { + label: 'IP Match', + value: `(function() { + return { + functions: { + /** + * ipMatch supports IP address and CIDR matching + * Example: + * - IP: 192.168.2.123 + * - CIDR: 192.168.2.0/24 + * - Match result: true + * + * Use case: Access control based on IP address + */ + ipMatch: true + }, + matchingForGFunction: undefined, + matchingDomainForGFunction: undefined + }; +})();` + }, + + globMatch: { + label: 'Glob Match', + value: `(function() { + return { + functions: { + /** + * globMatch supports Glob pattern matching + * Example: + * - Path: /alice_data/resource1 + * - Glob pattern: /alice_data/* + * - Match result: true + * + * Use case: File path matching, similar to shell wildcards + */ + globMatch: true + }, + matchingForGFunction: undefined, + matchingDomainForGFunction: undefined + }; +})();` + } +}; From a696449c190309d826252b4c4f3ca0fc228cf88b Mon Sep 17 00:00:00 2001 From: Coki Date: Tue, 10 Dec 2024 20:03:07 +0800 Subject: [PATCH 2/7] feat: add CustomConfigPanel component and enhance editor functionality --- app/components/editor/CustomConfigPanel.tsx | 339 ++++++++++++++++++++ app/components/editor/hooks/useRunTest.tsx | 6 + app/components/editor/index.tsx | 114 ++----- app/globals.css | 16 + 4 files changed, 395 insertions(+), 80 deletions(-) create mode 100644 app/components/editor/CustomConfigPanel.tsx diff --git a/app/components/editor/CustomConfigPanel.tsx b/app/components/editor/CustomConfigPanel.tsx new file mode 100644 index 0000000..fc8bde0 --- /dev/null +++ b/app/components/editor/CustomConfigPanel.tsx @@ -0,0 +1,339 @@ +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 newFunction = { + id: Date.now().toString(), + name: `my_func${functions.length + 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'; + }) + ) { + alert('Role Matching Function already exists!'); + 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'; + }) + ) { + alert('Domain Matching Function already exists!'); + 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); + }; + + 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/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 7e555ed..f532b85 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,16 +24,31 @@ 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 { CONFIG_TEMPLATES } from '@/app/constants/configTemplates'; +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(); - const [open, setOpen] = useState(true); + modelKind, + setModelKind, + modelText, + policy, + request, + echo, + setEcho, + requestResult, + setRequestResult, + customConfig, + share, + setShare, + enforceContextData, + setPolicyPersistent, + setModelTextPersistent, + setCustomConfigPersistent, + setRequestPersistent, + setEnforceContextDataPersistent, + handleShare, + } = useIndex(); + const [open, setOpen] = useState(false); const { enforcer } = useRunTest(); const { shareInfo } = useShareInfo(); const { copy } = useCopy(); @@ -95,87 +109,27 @@ export const EditorScreen = () => { }, [modelKind, modelText, policy, customConfig, request, enforceContextData, enforcer, setEcho, setRequestResult]); const textClass = clsx(theme === 'dark' ? 'text-gray-200' : 'text-gray-800'); - const handleTemplateChange = (e: React.ChangeEvent) => { - const template = CONFIG_TEMPLATES[e.target.value]; - if (template) { - setCustomConfigPersistent(template.value); - } - }; - return (
- - -
- {(showCustomConfig || open) && ( - <> - - - )} -
-
- {(showCustomConfig || open) && ( -
- -
- )} -
+
diff --git a/app/globals.css b/app/globals.css index 56c0343..f091258 100644 --- a/app/globals.css +++ b/app/globals.css @@ -14,3 +14,19 @@ 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; +} +/* ------------------------------ */ \ No newline at end of file From 010a7c01f28538e4d8670a2b1702c8c0ee81c819 Mon Sep 17 00:00:00 2001 From: Coki Date: Tue, 10 Dec 2024 20:17:46 +0800 Subject: [PATCH 3/7] feat: update custom config to custom functions and add new translations --- app/utils/contentExtractor.ts | 6 +++--- messages/ar.json | 5 ++++- messages/de.json | 5 ++++- messages/en.json | 5 ++++- messages/es.json | 5 ++++- messages/fr.json | 5 ++++- messages/id.json | 5 ++++- messages/it.json | 5 ++++- messages/ja.json | 5 ++++- messages/ko.json | 5 ++++- messages/ms.json | 5 ++++- messages/pt.json | 5 ++++- messages/ru.json | 5 ++++- messages/tr.json | 5 ++++- messages/vi.json | 5 ++++- messages/zh-Hant.json | 5 ++++- messages/zh.json | 5 ++++- 17 files changed, 67 insertions(+), 19 deletions(-) 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": "重置", From 9c783cd2130ae7961d971ac46e664a8f997bc28a Mon Sep 17 00:00:00 2001 From: Coki Date: Tue, 10 Dec 2024 20:25:25 +0800 Subject: [PATCH 4/7] feat: add defaultCustomConfig and update useIndex to use it --- app/components/editor/casbin-mode/example.ts | 17 ++ app/components/editor/hooks/useIndex.tsx | 7 +- app/constants/configTemplates.ts | 211 ------------------- 3 files changed, 20 insertions(+), 215 deletions(-) delete mode 100644 app/constants/configTemplates.ts diff --git a/app/components/editor/casbin-mode/example.ts b/app/components/editor/casbin-mode/example.ts index cdd6eed..1e739c2 100644 --- a/app/components/editor/casbin-mode/example.ts +++ b/app/components/editor/casbin-mode/example.ts @@ -430,6 +430,23 @@ 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 + }; +})();`; export const defaultEnforceContext = `{ "r": "r", "p": "p", diff --git a/app/components/editor/hooks/useIndex.tsx b/app/components/editor/hooks/useIndex.tsx index 6247c4b..4a8e2c5 100644 --- a/app/components/editor/hooks/useIndex.tsx +++ b/app/components/editor/hooks/useIndex.tsx @@ -1,8 +1,7 @@ import React, { isValidElement, ReactNode, useEffect, useRef, useState } from 'react'; -import { defaultEnforceContext, example, ModelKind } from '@/app/components/editor/casbin-mode/example'; +import { defaultCustomConfig, defaultEnforceContext, example, ModelKind } from '@/app/components/editor/casbin-mode/example'; import { ShareFormat } from '@/app/components/editor/hooks/useShareInfo'; import { defaultEnforceContextData } from '@/app/components/editor/hooks/useSetupEnforceContext'; -import { CONFIG_TEMPLATES } from '@/app/constants/configTemplates'; export default function useIndex() { const [modelKind, setModelKind] = useState('basic'); @@ -11,7 +10,7 @@ export default function useIndex() { const [request, setRequest] = useState(''); const [echo, setEcho] = useState(<>); const [requestResult, setRequestResult] = useState(''); - const [customConfig, setCustomConfig] = useState(CONFIG_TEMPLATES.default.value); + const [customConfig, setCustomConfig] = useState(''); const [share, setShare] = useState(''); const [triggerUpdate, setTriggerUpdate] = useState(0); const [enforceContextData, setEnforceContextData] = useState(new Map(defaultEnforceContextData)); @@ -73,7 +72,7 @@ export default function useIndex() { setPolicy(shared?.policy ?? example[modelKind].policy); setModelText(shared?.model ?? example[modelKind].model); setRequest(shared?.request ?? example[modelKind].request); - setCustomConfig(shared?.customConfig ?? CONFIG_TEMPLATES.default.value); + setCustomConfig(shared?.customConfig ?? defaultCustomConfig); setEnforceContextData(new Map(Object.entries(JSON.parse(shared?.enforceContext || example[modelKind].enforceContext || defaultEnforceContext)))); loadState.current.content = undefined; }, [modelKind, triggerUpdate]); diff --git a/app/constants/configTemplates.ts b/app/constants/configTemplates.ts deleted file mode 100644 index 1e56668..0000000 --- a/app/constants/configTemplates.ts +++ /dev/null @@ -1,211 +0,0 @@ -/** - * Casbin built-in matching function configuration templates - * - * All key matching functions follow this format: - * bool function_name(string url, string pattern) - * Returns a boolean indicating whether the URL matches the pattern. - */ - -export const CONFIG_TEMPLATES = { - default: { - label: 'default', - value: `(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 - }; -})();` - }, - - keyMatch: { - label: 'RESTful (KeyMatch)', - value: `(function() { - return { - functions: { - /** - * keyMatch supports * wildcard matching - * Example: - * - URL: /alice_data/resource1 - * - Pattern: /alice_data/* - * - Match result: true - * - * Use case: Simple wildcard URL matching - */ - keyMatch: true, - regexMatch: true - }, - matchingForGFunction: undefined, - matchingDomainForGFunction: undefined - }; -})();` - }, - - keyMatch2: { - label: 'RESTful (KeyMatch2)', - value: `(function() { - return { - functions: { - /** - * keyMatch2 supports :param parameter matching - * Example: - * - URL: /alice_data/resource1 - * - Pattern: /alice_data/:resource - * - Match result: true - * - * Use case: Parameter matching in RESTful APIs - */ - keyMatch2: true, - regexMatch: true - }, - matchingForGFunction: undefined, - matchingDomainForGFunction: undefined - }; -})();` - }, - - keyMatch3: { - label: 'RESTful (KeyMatch3)', - value: `(function() { - return { - functions: { - /** - * keyMatch3 supports {param} parameter matching - * Example: - * - URL: /alice_data/resource1 - * - Pattern: /alice_data/{resource} - * - Match result: true - * - * Use case: Parameter matching using curly braces - */ - keyMatch3: true, - regexMatch: true - }, - matchingForGFunction: undefined, - matchingDomainForGFunction: undefined - }; -})();` - }, - - keyMatch4: { - label: 'RESTful (KeyMatch4)', - value: `(function() { - return { - functions: { - /** - * keyMatch4 supports multiple identical parameter matching - * Example: - * - URL: /alice_data/123/book/123 - * - Pattern: /alice_data/{id}/book/{id} - * - Match result: true - * - * Use case: When multiple parameters in a URL need to have the same value - */ - keyMatch4: true, - regexMatch: true - }, - matchingForGFunction: undefined, - matchingDomainForGFunction: undefined - }; -})();` - }, - - keyMatch5: { - label: 'RESTful (KeyMatch5)', - value: `(function() { - return { - functions: { - /** - * keyMatch5 supports query parameter matching - * Example: - * - URL: /alice_data/123/?status=1 - * - Pattern: /alice_data/{id}/* - * - Match result: true - * - * Use case: Handling URL query parameters - */ - keyMatch5: true, - regexMatch: true - }, - matchingForGFunction: undefined, - matchingDomainForGFunction: undefined - }; -})();` - }, - - regexMatch: { - label: 'Regex Match', - value: `(function() { - return { - functions: { - /** - * regexMatch supports regular expression matching - * Example: - * - String: /alice_data/resource1 - * - Regex pattern: ^/alice_data/.*$ - * - Match result: true - * - * Use case: Complex string matching rules - */ - regexMatch: true - }, - matchingForGFunction: undefined, - matchingDomainForGFunction: undefined - }; -})();` - }, - - ipMatch: { - label: 'IP Match', - value: `(function() { - return { - functions: { - /** - * ipMatch supports IP address and CIDR matching - * Example: - * - IP: 192.168.2.123 - * - CIDR: 192.168.2.0/24 - * - Match result: true - * - * Use case: Access control based on IP address - */ - ipMatch: true - }, - matchingForGFunction: undefined, - matchingDomainForGFunction: undefined - }; -})();` - }, - - globMatch: { - label: 'Glob Match', - value: `(function() { - return { - functions: { - /** - * globMatch supports Glob pattern matching - * Example: - * - Path: /alice_data/resource1 - * - Glob pattern: /alice_data/* - * - Match result: true - * - * Use case: File path matching, similar to shell wildcards - */ - globMatch: true - }, - matchingForGFunction: undefined, - matchingDomainForGFunction: undefined - }; -})();` - } -}; From 6d201dcdae507f486d03b218dd3551f130320f34 Mon Sep 17 00:00:00 2001 From: Coki Date: Fri, 13 Dec 2024 00:43:09 +0800 Subject: [PATCH 5/7] fix: default expand sidebar --- app/components/editor/CustomConfigPanel.tsx | 36 ++++++++++---------- app/components/editor/casbin-mode/example.ts | 24 ++++++------- app/components/editor/index.tsx | 2 +- app/globals.css | 11 ++++++ 4 files changed, 41 insertions(+), 32 deletions(-) diff --git a/app/components/editor/CustomConfigPanel.tsx b/app/components/editor/CustomConfigPanel.tsx index fc8bde0..47081d8 100644 --- a/app/components/editor/CustomConfigPanel.tsx +++ b/app/components/editor/CustomConfigPanel.tsx @@ -202,6 +202,12 @@ export const CustomConfigPanel: React.FC = ({ }, 0); }; + const hasMatchingFunction = (name: string) => { + return functions.some((f) => { + return f.name === name; + }); + }; + return ( <> + + 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/index.tsx b/app/components/editor/index.tsx index f532b85..f46741b 100755 --- a/app/components/editor/index.tsx +++ b/app/components/editor/index.tsx @@ -48,7 +48,7 @@ export const EditorScreen = () => { setEnforceContextDataPersistent, handleShare, } = useIndex(); - const [open, setOpen] = useState(false); + const [open, setOpen] = useState(true); const { enforcer } = useRunTest(); const { shareInfo } = useShareInfo(); const { copy } = useCopy(); diff --git a/app/globals.css b/app/globals.css index f091258..a264cea 100644 --- a/app/globals.css +++ b/app/globals.css @@ -29,4 +29,15 @@ button:hover img { ::-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 From 249b1b60fbfdef5982d273a785b5d839ddd1c049 Mon Sep 17 00:00:00 2001 From: Coki Date: Fri, 13 Dec 2024 00:50:48 +0800 Subject: [PATCH 6/7] fix: correct function naming sequence for custom functions --- app/components/editor/CustomConfigPanel.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/components/editor/CustomConfigPanel.tsx b/app/components/editor/CustomConfigPanel.tsx index 47081d8..da23051 100644 --- a/app/components/editor/CustomConfigPanel.tsx +++ b/app/components/editor/CustomConfigPanel.tsx @@ -85,9 +85,13 @@ export const CustomConfigPanel: React.FC = ({ // 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${functions.length + 1}`, + name: `my_func${regularFunctionCount + 1}`, body: '(arg1, arg2) => {\n return arg1.endsWith(arg2);\n}', }; setFunctions([...functions, newFunction]); From b43b737a3252096279fdc2cb3d998fefc9a1db0a Mon Sep 17 00:00:00 2001 From: Coki Date: Fri, 13 Dec 2024 00:58:10 +0800 Subject: [PATCH 7/7] refactor: remove unnecessary alerts in CustomConfigPanel component --- app/components/editor/CustomConfigPanel.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/components/editor/CustomConfigPanel.tsx b/app/components/editor/CustomConfigPanel.tsx index da23051..3d7171e 100644 --- a/app/components/editor/CustomConfigPanel.tsx +++ b/app/components/editor/CustomConfigPanel.tsx @@ -85,9 +85,9 @@ export const CustomConfigPanel: React.FC = ({ // Add new function const addNewFunction = () => { - const regularFunctionCount = functions.filter((f) => - {return !['matchingForGFunction', 'matchingDomainForGFunction'].includes(f.name)} - ).length; + const regularFunctionCount = functions.filter((f) => { + return !['matchingForGFunction', 'matchingDomainForGFunction'].includes(f.name); + }).length; const newFunction = { id: Date.now().toString(), @@ -126,7 +126,6 @@ export const CustomConfigPanel: React.FC = ({ return f.name === 'matchingForGFunction'; }) ) { - alert('Role Matching Function already exists!'); return; } @@ -148,7 +147,6 @@ export const CustomConfigPanel: React.FC = ({ return f.name === 'matchingDomainForGFunction'; }) ) { - alert('Domain Matching Function already exists!'); return; }