From 397596b12c1173e32b467ce8071809af4d1399fd Mon Sep 17 00:00:00 2001
From: Sang Nguyen <sang.nguyen@lcsa.vn>
Date: Wed, 5 Mar 2025 08:43:15 +0700
Subject: [PATCH] upd: + add function csm create, update user role + add User
 Access Logic, Action, behavior, trait + Add workflow runtime Action,
 behavior, logic, trait

---
 README.md                                     | 180 +-----------------
 base/helper/LogHelper.php                     |  28 +--
 base/services/BaseService.php                 |   3 +-
 cnm/services/CnmService.php                   |  12 +-
 csm/actions/UserAccessAction.php              |  16 ++
 csm/command/CrawlController.php               |  36 ++--
 csm/command/PushController.php                |  84 ++++++++
 csm/enums/ApiEndpoint.php                     |   1 +
 csm/jobs/AssignUserRoleByObjectRoleJob.php    |   2 +-
 csm/jobs/RevokeUserRoleByObjectRoleJob.php    |   2 +-
 csm/models/LoginMethod.php                    |  23 +++
 csm/services/CsmService.php                   |  78 +++++++-
 csm/services/UserService.php                  |  35 +++-
 csm/traits/UserTrait.php                      |  14 ++
 mapper/behaviors/ObjectClauseBehavior.php     |   8 +-
 mapper/services/ObjectClauseService.php       |  12 +-
 mrbac/models/forms/ActionForm.php             |   4 +-
 mrbac/models/forms/ModelForm.php              |   2 +-
 mrbac/services/ModelService.php               |  14 +-
 workflow/actions/WorkflowRuntimeIndex.php     | 152 +++++++++++++++
 .../behaviors/InitWorkflowChangeBehavior.php  |  11 +-
 workflow/command/WorkflowController.php       |  26 +++
 workflow/services/WorkflowConfigService.php   |   4 +-
 workflow/services/WorkflowRuntimeService.php  |  26 ++-
 workflow/services/WorkflowService.php         |  30 +--
 workflow/traits/WorkflowRuntimeTrait.php      |  14 ++
 26 files changed, 528 insertions(+), 289 deletions(-)
 create mode 100644 csm/actions/UserAccessAction.php
 create mode 100644 csm/command/PushController.php
 create mode 100644 csm/models/LoginMethod.php
 create mode 100644 csm/traits/UserTrait.php
 create mode 100644 workflow/actions/WorkflowRuntimeIndex.php
 create mode 100644 workflow/command/WorkflowController.php
 create mode 100644 workflow/traits/WorkflowRuntimeTrait.php

diff --git a/README.md b/README.md
index 3a44cde..94e3062 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,6 @@
 ```
 docker-compose exec api composer require lsb/yii2-integrator
 ```
-
 ```
 docker-compose exec api yii migrate-module
 ```
@@ -25,11 +24,11 @@ docker-compose exec api yii migrate-module
     "options": {}
 },
 ```
-
 ```
 Config local để dev khĂ´ng push change composer.json nĂ y lĂªn 
 ```
 
+
 ### Add configuration console.php trong main project
 
 ```
@@ -39,183 +38,8 @@ Config local để dev khĂ´ng push change composer.json nĂ y lĂªn
             'class' => 'yii\console\controllers\MigrateController',
             'migrationPath' => [
                 '@lbs/workflow/migrations'
-                '@lbs/cnm/migrations'
-                '@lbs/mapper/migrations'
-                '@lbs/mrbac/migrations'
-                '@lbs/resource/migrations'
             ],
             'migrationTable' => 'migration_module'
         ],
     ]
-```
-
-### Add configuration log.php trong main project
-
-```
-   'log' => [
-    'traceLevel' => YII_DEBUG ? 3 : 0,
-    'targets' => [
-        [
-            'class' => 'yii\log\FileTarget',
-            'categories' => ['application-cnm'], // Custom category
-            'logFile' => '@runtime/logs/integrate-cnm.log', // Custom log file
-            'maxLogFiles' => 5, // Keep last 5 log files
-        ],
-        [
-            'class' => 'yii\log\FileTarget',
-            'categories' => ['application-csm'], // Custom category
-            'logFile' => '@runtime/logs/integrate-csm.log', // Custom log file
-            'maxLogFiles' => 5, // Keep last 5 log files
-        ],
-        [
-            'class' => 'yii\log\FileTarget',
-            'categories' => ['application-resource'], // Custom category
-            'logFile' => '@runtime/logs/integrate-resource.log', // Custom log file
-            'maxLogFiles' => 5, // Keep last 5 log files
-        ],
-        [
-            'class' => 'yii\log\FileTarget',
-            'categories' => ['application-workflow'], // Custom category
-            'logFile' => '@runtime/logs/integrate-workflow.log', // Custom log file
-            'maxLogFiles' => 5, // Keep last 5 log files
-        ],
-    ],
-]
-```
-
-### Add configuration for command
-
-```
-$config['bootstrap'][] = 'mrbac';
-$config['modules']['mrbac'] = [
-    'class' => \lsb\mrbac\Module::class,
-    'modelPaths' => [
-        "/app/models" => "app\\models",
-    ],
-    'tenants' => [ 'LCS']
-];
-```
-
-### Add configuration for gui mrbac
-
-```
-$config['bootstrap'][] = 'mrbac';
-$config['modules']['mrbac'] = [
-    'class' => \lsb\mrbac\Module::class,
-    'modelPaths' => [
-        "/app/models" => "app\\models",
-    ],
-    'tenants' => [ 'LCS']
-];
-```
-
-### Add configuration for gui csm
-
-```
-$config['bootstrap'][] = 'csmui';
-$config['modules']['csmui'] = [
-    'class' => \lsb\csm\Module::class,
-    'queue' => 'queue',
-    'modelPaths' => [
-        "/app/models" => "app\\models",
-    ],
-    'tenant' => 'LCS'
-];
-```
-
-### Modules guideline
-
--   #### Workflow
-0. Define `workflow` object config in main app
-```php
-<?php
-return [
-    'class' => \lsb\workflow\models\Workflow::class,
-    'baseUrl' => getenv('WORKFLOW_BASE_URL'),
-    'username' => getenv('WORKFLOW_USERNAME'),
-    'password' => getenv('WORKFLOW_PASSWORD'),
-    'appCode' => strtoupper(getenv('SERVICE_NAME')),
-];
-```
-
-1. Run migration to init `workflow_config` and `workflow_reference` and `workflow_runtime`
-
-`workfow_config`: use to define any version of workflow
-
-`workflow_reference`: use to define which version will applied in object
-
-`workflow_runtime`: use to store one by one which user handle current state
-
-2. Init main model need to run workflow with behaviors
-
-```php
-public Foo extends Model {
-    public function behaviors()
-    {
-        return [
-            'initWorkflow' => [
-                'class' => InitWorkflowChangeBehavior::class,
-                'silent_error' => true,
-                'workflow_ref_code' => 'WORKFLOW_REF_CODE_FOO', // these code is used for identify which workflow need to use defined in workflow_reference
-                'workflow_version' => 1,
-                'instance_code' => function ($model) {
-                    //TODO: Handle logic get the instance identity to compare with workflow engine
-                    return 'MODEL_INSTANCE_CODE';
-                },
-                'callback' => function ($model, $instanceCode, $workflowCode) {
-                    //TODO: Handle logic after workflow initialized
-                }
-            ]
-        ]);
-    }
-    // Workflow will be initialized and started after Foo object saved.
-    // The runtime will be sync and use interface `getAllowAction`, `executeAction` to change node
-}
-```
-
-3. Define workflow with `workflow-engine` and run command
-
-```shell
-# these command use to mapping workflow version with ref code
-$ docker-compose exec api php yii workflow/init <workflow_code> <ref_code>
-
-# example:
-$ docker-compose exec api php yii workflow/init WORKFLOW_VERSION_1.0.1 WORKFLOW_REF_CODE_FOO
-```
-
-4. Define workflow callback controller and action to matches with `urls` declared in `workflow-engine`
-5. Use workflow definitions interfaces In `WorkflowService` and `WorkflowRuntimeService` to handle
-
-```php
-// Context: This Foo object instance have to run in 3 step
-// + NEW: after created and start workflow
-// + PROCESS: after execute action in `NEW` node 
-// + DONE: after execute action in `PROCESS` node 
-//  ---------             ---------               ---------
-// |  NEW    |   ---->  |  PROCESS |    ---->    |  DONE   |
-//  ---------             ---------               ---------
-
-// Use basic step after Foo object initialized workflow
-// At the moment `Foo` instance is in `NEW`
-
-// (1) Exec: WorkflowService::getAllowActions(<workflow_define>,<runtime_id>, <params>, <current_jwt>)
-//      -> Call `Workflow-engine` for return list available action
-//      -> these function will return list action and pick one <action_code> for next step
-//   
-// (2) Exec: WorkflowService::executeActionAndSaveRuntimes(<workflow_define>,<runtime_id>, <action_code>, <note> , <current_jwt>)
-//      -> call `Workflow-engine` for execute <action_code> with <runtime_id> when success, this function call `workflow-engine` again to save `workflow_runtimes`
-//      -> these function will return state of current `instance` you must to save it.
-
-// And then the `Foo` instance will change to `PROCESS`. Repeat step (1), (2) and `Foo` instance get `DONE`
-
-// *** Attentions:
-// 1. <workflow_define> must be defined.
-// 2. <runtime_id> get from `workflow_runtime.runtime_id` .
-// 3. <current_jwt> use current jwt to identity who handle in workflow.
-// 4. Always call get workflow runtime after execute to ensure your version `workflow_runtime` data matches with `workflow-engine`.
-// 6. Everytime and everywhere the `instance_code` must be unique.
-// 5. while an error appeared in your process, workflow still run.
-// 7. Never think about the opening a DB transaction in your code when you save this object, workflow can not check and get data throw `instance_code` if data isn't written in DB.
-
-// Thanks for your attention. Good luck !! =]]
-```
+```
\ No newline at end of file
diff --git a/base/helper/LogHelper.php b/base/helper/LogHelper.php
index e8f1e53..aa8d7ee 100644
--- a/base/helper/LogHelper.php
+++ b/base/helper/LogHelper.php
@@ -8,28 +8,10 @@ use yii\helpers\BaseConsole;
 
 class LogHelper
 {
-    /**
-     * @param $message
-     * @param $tracing_key
-     * @param $options
-     * - ```php
-     * [
-     *      'category' => 'application',
-     *      'fg' =>  BaseConsole::FG_BLACK
-     * ]
-     * ```
-     * - `fg`: array $format An array containing formatting values.
-     *  You can pass any of the `FG_*`, `BG_*` and `TEXT_*` constants
-     *  and also [[xtermFgColor]] and [[xtermBgColor]] to specify a format.
-     * - `context`: string the category of the message.
-     * @return void
-     */
-    public static function logMes($message, $tracing_key = [], $options = [])
+    public static function logMes($message, $tracing_key = null, $fg = 'lbs')
     {
         $bt = debug_backtrace();
         $caller = $bt[1]['function'] ?? null;
-        $category = $options['category'] ?? 'application';
-        $fg = $options['fg'] ?? '';
         if (Yii::$app instanceof Application) {
             $date = \Yii::$app->formatter->asDatetime(time());
             if (is_array($message)) {
@@ -38,13 +20,13 @@ class LogHelper
             if(is_array($tracing_key)){
                 $tracing_key = implode("][", $tracing_key);
             }
-            Yii::error("[$caller][$date][$tracing_key]" . " " . $message, $category);
-            BaseConsole::output(BaseConsole::ansiFormat("[$caller][$date][$tracing_key]" . " " . $message, $fg));
+            Yii::error("[$caller][$date][$tracing_key]" . " " . $message, $fg);
+            BaseConsole::output("[$caller][$date][$tracing_key]" . " " . $message);
         } else {
             if (is_string($message)) {
-                Yii::error("[$caller] " . $message, $category);
+                Yii::error("[$caller] " . $message, $fg);
             } else {
-                Yii::error("[$caller] " . json_encode($message), $category);
+                Yii::error("[$caller] " . json_encode($message), $fg);
             }
         }
     }
diff --git a/base/services/BaseService.php b/base/services/BaseService.php
index 23f2a64..e315a04 100644
--- a/base/services/BaseService.php
+++ b/base/services/BaseService.php
@@ -69,6 +69,7 @@ class BaseService
 //        $request->withAddedHeader('Content-Type', $this->getContentType()->value);
         $endpoint = "{$this->baseUrl}{$endpoint}";
         LogHelper::logMes("$endpoint: ", ["call"]);
+        LogHelper::logMes("header: ".json_encode($headers), ["call"]);
         LogHelper::logMes(json_encode($body), ["Call"]);
         $response = $this->_client->request($method->name, $endpoint, [
             $contentType->value => $body,
@@ -164,4 +165,4 @@ class BaseService
     {
         return new BaseService($baseUrl, $authenticator, $contentType);
     }
-}
\ No newline at end of file
+}
diff --git a/cnm/services/CnmService.php b/cnm/services/CnmService.php
index 4c829ea..1ab3d39 100644
--- a/cnm/services/CnmService.php
+++ b/cnm/services/CnmService.php
@@ -52,7 +52,7 @@ class CnmService
                     'response' => json_encode($contents),
                 ], '');
                 if (!$notifyHistory->save()) {
-                    LogHelper::logMes("Save notify history failed!", $url, ['category' => 'application-cnm']);
+                    LogHelper::logMes("Save notify history failed!");
                 }
 
                 $rp[] = $contents;
@@ -74,7 +74,7 @@ class CnmService
                 'response' => json_encode($contents),
             ], '');
             if (!$notifyHistory->save()) {
-                LogHelper::logMes("Save notify history failed!", $url, ['category' => 'application-cnm']);
+                LogHelper::logMes("Save notify history failed!");
             }
             $successSend = [];
 
@@ -87,7 +87,7 @@ class CnmService
                             if ($status >= 400 && $hasError) {
                                 $check = self::deleteUserDevice($key);
                                 if (!$check) {
-                                    LogHelper::logMes("Delete UserDeviceKey failed with code [ $key ] ", $url, ['category' => 'application-cnm']);
+                                    LogHelper::logMes("Delete UserDeviceKey failed with code [ $key ] ");
                                 }
                             } else {
                                 $userCode = UserDeviceRegister::findOne(['device_token' => $key])->user_code ?? null;
@@ -95,7 +95,7 @@ class CnmService
                             }
                         }
                     } else {
-                        LogHelper::logMes("Fail logic with response [ $cts ] ", $url, ['category' => 'application-cnm']);
+                        LogHelper::logMes("Fail logic with response [ $cts ] ");
                     }
                 }
             }
@@ -121,7 +121,7 @@ class CnmService
         foreach ($listUserCodes as $userCode => $content) {
             if (!isset($content['message'])) {
                 $jeE = json_encode($content);
-                LogHelper::logMes("Create notification failed with content $jeE", $userCode, ['category' => 'application-cnm']);
+                LogHelper::logMes("Create notification failed with content $jeE");
                 continue;
             }
             $jsonMessage = json_decode($content['message'], true);
@@ -143,7 +143,7 @@ class CnmService
                 'user_code' => $userCode,
             ], '');
             if (!$notification->save()) {
-                LogHelper::logMes("Create notification failed with objectCode [ $objectCode ] and identity [ $identity ]", $userCode, ['category' => 'application-cnm']);
+                LogHelper::logMes("Create notification failed with objectCode [ $objectCode ] and identity [ $identity ]");
             }
         }
     }
diff --git a/csm/actions/UserAccessAction.php b/csm/actions/UserAccessAction.php
new file mode 100644
index 0000000..a4ab3b4
--- /dev/null
+++ b/csm/actions/UserAccessAction.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace lsb\csm\actions;
+
+use lsb\csm\services\UserService;
+use yii\base\Action;
+
+class UserAccessAction extends Action
+{
+    public $modelClass;
+
+    public function run()
+    {
+        return UserService::getUserAccess($this->modelClass);
+    }
+}
diff --git a/csm/command/CrawlController.php b/csm/command/CrawlController.php
index 305e45b..dff636d 100644
--- a/csm/command/CrawlController.php
+++ b/csm/command/CrawlController.php
@@ -3,6 +3,8 @@
 namespace lsb\csm\command;
 
 use lsb\base\helper\LogHelper;
+use lsb\csm\models\Csm;
+use lsb\csm\models\LoginMethod;
 use lsb\csm\models\Role;
 use lsb\csm\models\User;
 use lsb\csm\models\UserRole;
@@ -22,7 +24,7 @@ class CrawlController extends Controller
         $app = CsmService::getApp($csm, $token);
         $roles = CsmService::getRoles($csm, $app['id'], $token);
         foreach ($roles as $role) {
-            LogHelper::logMes("init role : " . $role['role_name'], '', ['category' => 'application-csm']);
+            LogHelper::logMes("init role : " . $role['role_name']);
             $appRole = Role::findOne(['code' => $role['role_name']]);
             if (empty($appRole)) {
                 $appRole = new Role();
@@ -32,7 +34,7 @@ class CrawlController extends Controller
 
 
             if (!$appRole->save()) {
-                LogHelper::logMes("errors: " . json_encode($appRole->errors), '', ['category' => 'application-csm']);
+                LogHelper::logMes("errors: " . json_encode($appRole->errors));
             }
             $rolesMapping[$role['id']] = $appRole->id;
         }
@@ -56,7 +58,7 @@ class CrawlController extends Controller
             $client = $user['client'];
 
             $appUser = User::findOne(['user_code' => $user['user_code']]);
-            LogHelper::logMes("init user : " . $user['user_code'], '', ['category' => 'application-csm']);
+            LogHelper::logMes("init user : " . $user['user_code']);
             if (empty($appUser)) {
                 $appUser = new User();
             }
@@ -67,7 +69,7 @@ class CrawlController extends Controller
             $appUser->phone = $client['phone_number'];
 
             if (!$appUser->save()) {
-                LogHelper::logMes("errors: " . json_encode($appUser->errors), '', ['category' => 'application-csm']);
+                LogHelper::logMes("errors: " . json_encode($appUser->errors));
             }
 
             $mappingUsers[$userId] = $appUser->id;
@@ -109,9 +111,9 @@ class CrawlController extends Controller
 
         foreach ($userRoles as $key => $data) {
 
-            LogHelper::logMes("init user role: user_code - " . $key, '', ['category' => 'application-csm']);
+            LogHelper::logMes("init user role: user_code - " . $key);
             if (!isset($appUsers[$key])) {
-                LogHelper::logMes("init user role: user_code - $key  failed !!", '', ['category' => 'application-csm']);
+                LogHelper::logMes("init user role: user_code - $key  failed !!");
                 continue;
             }
             $appUser = $appUsers[$key];
@@ -119,20 +121,20 @@ class CrawlController extends Controller
             $appUserRoles = UserRole::findAll(['user_id' => $appUser['id']]);
             $appUserRoleIds = array_column($appUserRoles, 'role_id');
             foreach ($data as $role) {
-                LogHelper::logMes("init user role: role - " . $role['role_name'], '', ['category' => 'application-csm']);
+                LogHelper::logMes("init user role: role - " . $role['role_name']);
                 if (!isset($appRoles[$role['role_name']])) {
-                    LogHelper::logMes("init user role failed " . $role['role_name'] . " not existed !!", '', ['category' => 'application-csm']);
+                    LogHelper::logMes("init user role failed " . $role['role_name'] . " not existed !!");
                     continue;
                 }
                 $appRole = $appRoles[$role['role_name']];
                 if (in_array($appRole['id'], $appUserRoleIds)) {
-                    LogHelper::logMes("UserRole is existed", '', ['category' => 'application-csm']);
+                    LogHelper::logMes("UserRole is existed");
                 } else {
                     $appUserRole = new UserRole(['user_id' => $appUser['id'], 'role_id' => $appRole['id']]);
                     if (!$appUserRole->save()) {
-                        LogHelper::logMes("errors: " . json_encode($appUserRole->errors), '', ['category' => 'application-csm']);
+                        LogHelper::logMes("errors: " . json_encode($appUserRole->errors));
                     } else {
-                        LogHelper::logMes("save success !!", '', ['category' => 'application-csm']);
+                        LogHelper::logMes("save success !!");
                     }
                 }
                 if ($resetUserRoleObject) {
@@ -167,12 +169,12 @@ class CrawlController extends Controller
         echo $tenantCode . "\n";
         $csm = \Yii::$app->csm;
         $token = CsmService::getToken($csm);
-        LogHelper::logMes("Token: $token ", '', ['category' => 'application-csm']);
+        LogHelper::logMes("Token: $token ");
         self::resetIdentity($tenantCode);
         $tenant = CsmService::getTenant($csm, $tenantCode, $token);
         $app = CsmService::getApp($csm, $token);
         $tenantApp = CsmService::getTenantApp($csm, $tenant['id'], $app['id'], $token);
-        if(strpos($emails, ",")){
+        if (strpos($emails, ",")) {
             $emails = explode(",", $emails);
         } else {
             $emails = [$emails];
@@ -184,8 +186,8 @@ class CrawlController extends Controller
 
             foreach ($users as $user) {
 
-                LogHelper::logMes("user: ".json_encode($user), '', ['category' => 'application-csm']);
-                LogHelper::logMes("reset user: $email - $password", '', ['category' => 'application-csm']);
+                LogHelper::logMes("user: " . json_encode($user));
+                LogHelper::logMes("reset user: $email - $password");
                 $result = CsmService::updateUserPassword($csm, $user['id'], [
                     "email" => $email,
                     "full_name" => $user['client']['full_name'],
@@ -195,9 +197,9 @@ class CrawlController extends Controller
                     "app_id" => $app['id'],
                     "login_method" => $user['loginMethod'],
                 ], $token);
-                LogHelper::logMes("result: " . json_encode($result), '', ['category' => 'application-csm']);
+                LogHelper::logMes("result: " . json_encode($result));
             }
         }
     }
 
-}
\ No newline at end of file
+}
diff --git a/csm/command/PushController.php b/csm/command/PushController.php
new file mode 100644
index 0000000..6751130
--- /dev/null
+++ b/csm/command/PushController.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace lsb\csm\command;
+
+use lsb\base\helper\LogHelper;
+use lsb\csm\models\Csm;
+use lsb\csm\models\LoginMethod;
+use lsb\csm\models\User;
+use lsb\csm\services\CsmService;
+use yii\console\Controller;
+
+class PushController extends Controller
+{
+
+    public function actionCreateUser($email, $fullName, $phone, $password, $tenantCode, $appCode, $loginMethod = 1)
+    {
+        echo $tenantCode . "\n";
+        /**
+         * @var $csm Csm
+         */
+        $csm = \Yii::$app->csm;
+        $csm->appCode = $appCode;
+        $token = CsmService::getToken($csm);
+        $app = CsmService::getApp($csm, $token);
+        $tenant = CsmService::getTenant($csm, $tenantCode, $token);
+        switch ($loginMethod) {
+            case 2:
+                $loginMethod = new LoginMethod(true, false, true);
+                break;
+            case 3:
+                $loginMethod = new LoginMethod(true, true, true);
+                break;
+            case 4:
+                $loginMethod = new LoginMethod(false, false, true);
+                break;
+            case 5:
+                $loginMethod = new LoginMethod(false, true, true);
+                break;
+            case 6:
+                $loginMethod = new LoginMethod(false, true, false);
+                break;
+            case 1:
+            default:
+                $loginMethod = new LoginMethod(true, false, false);
+        }
+        CsmService::createUser($csm, $email, $fullName, $phone, $password, $loginMethod, $tenant['id'], $app['id'], $token);
+    }
+
+    public function actionAddUserRole($username, $roleName, $tenantCode, $appCode, $isRemove = false)
+    {
+        /**
+         * @var $csm Csm
+         */
+        $csm = \Yii::$app->csm;
+        $csm->appCode = $appCode;
+        $token = CsmService::getToken($csm);
+
+        $app = CsmService::getApp($csm, $token);
+        $tenant = CsmService::getTenant($csm, $tenantCode, $token);
+        $tenantApp = CsmService::getTenantApp($csm, $tenant['id'], $app['id'], $token);
+
+        $role = CsmService::getRoles($csm, $app['id'], $token, [
+            'filter[role_name]' => $roleName
+        ]);
+        LogHelper::logMes(json_encode($role));
+        $role = $role[0] ?? [];
+
+        $client = CsmService::getClient($csm, $tenant['id'], $token, [
+            'filter[email]' => $username,
+        ]);
+        $client = $client[0] ?? [];
+        LogHelper::logMes(json_encode($client));
+        $users = CsmService::getUsers($csm, $tenantApp['id'], $token, [
+            'filter[client_id]' => $client['id'],
+        ]);
+        LogHelper::logMes(json_encode($users));
+        $users = $users[0] ?? [];
+
+        CsmService::updateUserRole($csm, $users['id'], [
+            $role['id']
+        ], $token, $isRemove);
+    }
+
+}
diff --git a/csm/enums/ApiEndpoint.php b/csm/enums/ApiEndpoint.php
index 3ba47b6..d1da2b1 100644
--- a/csm/enums/ApiEndpoint.php
+++ b/csm/enums/ApiEndpoint.php
@@ -11,6 +11,7 @@ enum ApiEndpoint: string implements Api
     case MSPCSM_TENANT = "/mspcsm/tenants";
     case MSPCSM_TENANT_APP = "/mspcsm/tenant-apps";
     case MSPCSM_USERS = "/mspcsm/users";
+    case MSPCSM_CLIENTS = "/mspcsm/clients";
     case MSPCSM_ROLES = "/mspcsm/roles";
     case MSPCSM_USER_UPDATE_ROLE = "/mspcsm/users/update-role";
     case MSPCSM_USER_ROLES = "/mspcsm/user-roles";
diff --git a/csm/jobs/AssignUserRoleByObjectRoleJob.php b/csm/jobs/AssignUserRoleByObjectRoleJob.php
index c78c63c..b6f13f0 100644
--- a/csm/jobs/AssignUserRoleByObjectRoleJob.php
+++ b/csm/jobs/AssignUserRoleByObjectRoleJob.php
@@ -20,7 +20,7 @@ class AssignUserRoleByObjectRoleJob extends Model implements JobInterface
         $userRoles = UserService::findAllUserRoleByRole([$this->roleCode]);
         foreach ($userRoles as $userRole) {
             foreach ($this->objectIds as $objectId) {
-                LogHelper::logMes("Assign user_role:$userRole->id, object_id:$objectId, object_class:$this->objectClass", [$this->roleCode], ['category' => 'application-csm']);
+                LogHelper::logMes("Assign user_role:$userRole->id, object_id:$objectId, object_class:$this->objectClass", [$this->roleCode]);
                 UserService::saveUserRoleObject($userRole->id, $objectId, $this->objectClass);
             }
         }
diff --git a/csm/jobs/RevokeUserRoleByObjectRoleJob.php b/csm/jobs/RevokeUserRoleByObjectRoleJob.php
index 266d049..7bc85be 100644
--- a/csm/jobs/RevokeUserRoleByObjectRoleJob.php
+++ b/csm/jobs/RevokeUserRoleByObjectRoleJob.php
@@ -20,7 +20,7 @@ class RevokeUserRoleByObjectRoleJob extends Model implements JobInterface
         $userRoles = UserService::findAllUserRoleByRole([$this->roleCode]);
         foreach ($userRoles as $userRole) {
             foreach ($this->objectIds as $objectId) {
-                LogHelper::logMes("Revoke user_role:$userRole->id, object_id:$objectId, object_class:$this->objectClass", [$this->roleCode], ['category' => 'application-csm']);
+                LogHelper::logMes("Revoke user_role:$userRole->id, object_id:$objectId, object_class:$this->objectClass", [$this->roleCode]);
                 UserService::deleteUserRoleObject($userRole->id, $objectId, $this->objectClass);
             }
         }
diff --git a/csm/models/LoginMethod.php b/csm/models/LoginMethod.php
new file mode 100644
index 0000000..b1388b8
--- /dev/null
+++ b/csm/models/LoginMethod.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace lsb\csm\models;
+
+class LoginMethod
+{
+    public $user_pass = true;
+    public $sso = false;
+    public $otp = false;
+
+    /**
+     * @param bool $user_pass
+     * @param bool $sso
+     * @param bool $otp
+     */
+    public function __construct(bool $user_pass, bool $sso, bool $otp)
+    {
+        $this->user_pass = $user_pass;
+        $this->sso = $sso;
+        $this->otp = $otp;
+    }
+
+}
diff --git a/csm/services/CsmService.php b/csm/services/CsmService.php
index 46fe5f2..337487e 100644
--- a/csm/services/CsmService.php
+++ b/csm/services/CsmService.php
@@ -13,15 +13,41 @@ use lsb\base\helper\RequestHelper;
 use lsb\base\services\BaseService;
 use lsb\csm\enums\ApiEndpoint;
 use lsb\csm\models\Csm;
+use lsb\csm\models\LoginMethod;
 use yii\base\NotSupportedException;
 use yii\caching\CacheInterface;
 
 class CsmService
 {
 
-    public static function createUser(Csm $csm)
+    /**
+     * @param Csm $csm
+     * @param $email
+     * @param $full_name
+     * @param $phone
+     * @param $login_method
+     * @param $token
+     * @return mixed
+     * @throws \Psr\Http\Client\ClientExceptionInterface
+     */
+    public static function createUser(Csm $csm, $email, $full_name, $phone, $password, LoginMethod $login_method, $tenantId, $appId, $token)
     {
-        throw new NotSupportedException("not support now.");
+        if (empty($login_method)) {
+            $login_method = new LoginMethod(true, false, false);
+        }
+        $body = [
+            "email" => $email,
+            "full_name" => $full_name,
+            "phone" => $phone,
+            "password" => $password,
+            "login_method" => json_encode($login_method),
+            "tenant_id" => $tenantId,
+            "app_id" => $appId
+        ];
+        $result = self::getBaseService($csm->baseUrl)
+            ->addAuthenticator(new HttpBearer($token))
+            ->post(ApiEndpoint::MSPCSM_USERS->getEndPoint("/", $csm->appCode), $body);
+        return json_decode($result->getContents(), true);
     }
 
     public static function getApp(Csm $csm, $token)
@@ -107,6 +133,26 @@ class CsmService
         return $items;
     }
 
+    public static function getClient(Csm $csm, $tenantId, $token, $extraFilter = [])
+    {
+        $filters = [
+            'filter[record_status]' => 'O',
+            'filter[tenant_id]' => $tenantId,
+            'page' => '-1',
+            'per-page' => '-1',
+            'sort' => '-checker_date',
+            'keywords' => '',
+            'fields' => '',
+        ];
+        $filters = array_merge($filters, $extraFilter);
+        $result = self::getBaseService($csm->baseUrl)
+            ->addAuthenticator(new HttpBearer($token))
+            ->get(ApiEndpoint::MSPCSM_CLIENTS->getEndPoint('?', RequestHelper::arrToParams($filters)));
+        $results = json_decode($result->getContents(), true);
+        $items = $results['items'] ?? [];
+        return $items;
+    }
+
 
     public static function updateUserPassword(Csm $csm, $userId, $payload, $token)
     {
@@ -117,9 +163,9 @@ class CsmService
         return $results;
     }
 
-    public static function getRoles(Csm $csm, $appId, $token)
+    public static function getRoles(Csm $csm, $appId, $token, $filters = [])
     {
-        $filters = [
+        $filters = array_merge([
             'filter[record_status]' => 'O',
             'filter[app_id]' => $appId,
             'page' => '-1',
@@ -127,7 +173,7 @@ class CsmService
             'sort' => '-checker_date',
             'keywords' => '',
             'fields' => '',
-        ];
+        ], $filters);
 
         $result = self::getBaseService($csm->baseUrl)
             ->addAuthenticator(new HttpBearer($token))
@@ -172,6 +218,26 @@ class CsmService
         return $items;
     }
 
+    /**
+     * @param Csm $csm
+     * @param $roleIds
+     * @param $token
+     * @return array|mixed
+     * @throws \Psr\Http\Client\ClientExceptionInterface
+     */
+    public static function updateUserRole(Csm $csm, $userId, $roleIds, $token, $isRemove = false)
+    {
+        $payload = [
+            "role" => $roleIds,
+            'isRemove' => $isRemove
+        ];
+
+        $result = self::getBaseService($csm->baseUrl)
+            ->addAuthenticator(new HttpBearer($token))
+            ->post(ApiEndpoint::MSPCSM_USER_UPDATE_ROLE->getEndPoint("/", $userId), $payload);
+        return json_decode($result->getContents(), true);
+    }
+
     /**
      * @param Csm $csm
      * @return mixed
@@ -181,7 +247,7 @@ class CsmService
      */
     public static function getLogin(Csm $csm)
     {
-        $result = self::getBaseService($csm->gatewayUrl)->post(GatewayApi::API_ROOT->getEndPoint('/','authenticate'), [
+        $result = self::getBaseService($csm->gatewayUrl)->post(GatewayApi::API_ROOT->getEndPoint('/', 'authenticate'), [
             'username' => $csm->username,
             'password' => $csm->password,
             'rememberMe' => false,
diff --git a/csm/services/UserService.php b/csm/services/UserService.php
index ab87386..d325adf 100644
--- a/csm/services/UserService.php
+++ b/csm/services/UserService.php
@@ -9,6 +9,8 @@ use lsb\csm\models\Role;
 use lsb\csm\models\User;
 use lsb\csm\models\UserRole;
 use lsb\csm\models\UserRoleObject;
+use lsb\mrbac\models\FunctionCondition;
+use lsb\mrbac\services\ModelAccessService;
 use yii\base\InvalidConfigException;
 use yii\db\ActiveRecord;
 use yii\web\NotFoundHttpException;
@@ -194,11 +196,11 @@ class UserService
         if (empty($roleObject)) {
             $roleObject = new ObjectRole(['role_id' => $role->id, 'object_id' => $objectId, 'object_type' => $class]);
             if (!$roleObject->save()) {
-                LogHelper::logMes("save role object failed 'role_id' => $role->id, 'object_id' => $objectId, 'object_type' => $class: " . json_encode($roleObject->errors),'', ['category' => 'application-csm']);
+                LogHelper::logMes("save role object failed 'role_id' => $role->id, 'object_id' => $objectId, 'object_type' => $class: " . json_encode($roleObject->errors));
                 return;
             };
         } else {
-            LogHelper::logMes("User role object failed 'role_id' => $role->id, 'object_id' => $objectId, 'object_type' => $class: existed. ",'', ['category' => 'application-csm']);
+            LogHelper::logMes("User role object failed 'role_id' => $role->id, 'object_id' => $objectId, 'object_type' => $class: existed. ");
         }
     }
 
@@ -218,7 +220,7 @@ class UserService
             }
             $roleObject = new ObjectRole(['role_id' => $roleId, 'object_id' => $objectId, 'object_type' => $objectClass]);
             if (!$roleObject->save()) {
-                LogHelper::logMes("save role object failed 'role_id' => $roleId, 'object_id' => $objectId, 'object_type' => $objectClass: " . json_encode($roleObject->errors),'', ['category' => 'application-csm']);
+                LogHelper::logMes("save role object failed 'role_id' => $roleId, 'object_id' => $objectId, 'object_type' => $objectClass: " . json_encode($roleObject->errors));
             };
         }
         return true;
@@ -254,7 +256,7 @@ class UserService
 
         $roleObject = ObjectRole::findOne(['role_id' => $role->id, 'object_id' => $objectId, 'object_type' => $class]);
         if (empty($roleObject)) {
-            LogHelper::logMes("Role $roleCode - object $objectId not found",'', ['category' => 'application-csm']);
+            LogHelper::logMes("Role $roleCode - object $objectId not found");
         } else {
             $roleObject->delete();
         }
@@ -277,7 +279,7 @@ class UserService
         if (empty($userRole)) {
             $userRole = new UserRole(['user_id' => $user->id, 'role_id' => $role->id]);
             if (!$userRole->save()) {
-                LogHelper::logMes("save user role false $username, $roleCode, $objectId: " . json_encode($userRole->errors),'', ['category' => 'application-csm']);
+                LogHelper::logMes("save user role false $username, $roleCode, $objectId: " . json_encode($userRole->errors));
                 return;
             };
         }
@@ -302,7 +304,7 @@ class UserService
         if (empty($userRole)) {
             $userRole = new UserRole(['user_id' => $user->id, 'role_id' => $role->id]);
             if (!$userRole->save()) {
-                LogHelper::logMes("save user role false $username, $roleCode, $objectId: " . json_encode($userRole->errors),'', ['category' => 'application-csm']);
+                LogHelper::logMes("save user role false $username, $roleCode, $objectId: " . json_encode($userRole->errors));
                 return;
             }
         }
@@ -319,11 +321,11 @@ class UserService
         if (empty($userRoleObject)) {
             $userRoleObject = new UserRoleObject(['user_role_id' => $userRoleId, 'object_id' => $objectId, 'object_type' => $objectClass]);
             if (!$userRoleObject->save()) {
-                LogHelper::logMes("save user role object failed 'user_role_id' => $userRoleId, 'object_id' => $objectId, 'object_type' => $objectClass: " . json_encode($userRoleObject->errors),'', ['category' => 'application-csm']);
+                LogHelper::logMes("save user role object failed 'user_role_id' => $userRoleId, 'object_id' => $objectId, 'object_type' => $objectClass: " . json_encode($userRoleObject->errors));
                 return;
             };
         } else {
-            LogHelper::logMes("User role object failed 'user_role_id' => $userRoleId, 'object_id' => $objectId, 'object_type' => $objectClass: existed. ",'', ['category' => 'application-csm']);
+            LogHelper::logMes("User role object failed 'user_role_id' => $userRoleId, 'object_id' => $objectId, 'object_type' => $objectClass: existed. ");
         }
     }
 
@@ -359,7 +361,7 @@ class UserService
 
         $userRoleObject = UserRoleObject::findOne(['user_role_id' => $userRole->id, 'object_id' => $objectId, 'object_type' => $class]);
         if (empty($userRoleObject)) {
-            LogHelper::logMes("User role object is not existed 'user_role_id' => $userRole->id, 'object_id' => $objectId, 'object_type' => $class: existed. ",'', ['category' => 'application-csm']);
+            LogHelper::logMes("User role object is not existed 'user_role_id' => $userRole->id, 'object_id' => $objectId, 'object_type' => $class: existed. ");
         } else {
             $userRoleObject = new UserRoleObject(['user_role_id' => $userRole->id, 'object_id' => $objectId, 'object_type' => $class]);
             $userRoleObject->delete();
@@ -382,4 +384,17 @@ class UserService
     {
         return User::findOne(['email' => $email]);
     }
-}
\ No newline at end of file
+
+
+    public static function getUserInstanceAccess(ActiveRecord $instance, $nodes = [])
+    {
+        $functionCondition = self::findAllObjectIdByObjectTypeAndRoleAndUserCode(AuthorizationService::getUserCode(), AuthorizationService::getRoles(), FunctionCondition::class);
+        return ModelAccessService::checkInstanceAccess($instance, $functionCondition, $nodes);
+    }
+
+    public static function getUserAccess($modelClass)
+    {
+        $functionCondition = self::findAllObjectIdByObjectTypeAndRoleAndUserCode(AuthorizationService::getUserCode(), AuthorizationService::getRoles(), FunctionCondition::class);
+        return ModelAccessService::checkAccess($modelClass, $functionCondition);
+    }
+}
diff --git a/csm/traits/UserTrait.php b/csm/traits/UserTrait.php
new file mode 100644
index 0000000..baa34e9
--- /dev/null
+++ b/csm/traits/UserTrait.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace lsb\csm\traits;
+
+
+use lsb\csm\services\UserService;
+
+trait UserTrait
+{
+    public function getAccessActions()
+    {
+        return UserService::getUserInstanceAccess($this);
+    }
+}
diff --git a/mapper/behaviors/ObjectClauseBehavior.php b/mapper/behaviors/ObjectClauseBehavior.php
index 229d20d..4018370 100644
--- a/mapper/behaviors/ObjectClauseBehavior.php
+++ b/mapper/behaviors/ObjectClauseBehavior.php
@@ -104,15 +104,15 @@ class ObjectClauseBehavior extends \yii\base\Behavior
             $owner = $this->owner;
             foreach ($expressions as $expression) {
                 $isOK = $expressionLanguage->evaluate($expression->clause, $nodes);
-                LogHelper::logMes("$this->objectClass evaluate $expression->clause: " . ($isOK ? 'true' : 'false'),'', ['category' => 'application-mapper']);
-                LogHelper::logMes("$this->objectClass node : " . json_encode($nodes),'', ['category' => 'application-mapper']);
-                LogHelper::logMes("$this->objectClass instance : " . json_encode($owner->getAttributes()),'', ['category' => 'application-mapper']);
+                LogHelper::logMes("$this->objectClass evaluate $expression->clause: " . ($isOK ? 'true' : 'false'));
+                LogHelper::logMes("$this->objectClass node : " . json_encode($nodes));
+                LogHelper::logMes("$this->objectClass instance : " . json_encode($owner->getAttributes()));
                 $result = $expression->result;
                 switch ($expression->result_type) {
                     case ObjectClauseService::TYPE_EXPRESSION:
                         // if result type is a clause use Expression to re-evaluate again to get final result
                         $result = $expressionLanguage->evaluate($expression->result, $nodes);
-                        LogHelper::logMes("$this->objectClass re-evaluate $expression->result: $result",'', ['category' => 'application-mapper']);
+                        LogHelper::logMes("$this->objectClass re-evaluate $expression->result: $result");
                         break;
                 }
 
diff --git a/mapper/services/ObjectClauseService.php b/mapper/services/ObjectClauseService.php
index 98c5ab5..1b82b97 100644
--- a/mapper/services/ObjectClauseService.php
+++ b/mapper/services/ObjectClauseService.php
@@ -34,31 +34,31 @@ class ObjectClauseService
     {
         $expressionLanguage = new ExpressionLanguage();
         $expressionLanguage->registerProvider(new ArrayExpressionLanguageProvider());
-        LogHelper::logMes("$objectClass node : " . json_encode($nodes),'', ['category' => 'application-mapper']);
+        LogHelper::logMes("$objectClass node : " . json_encode($nodes));
 
         $result = null;
         $expressions = ObjectClauseService::getModelClauses($objectClass);
         foreach ($expressions as $expression) {
             $isOK = $expressionLanguage->evaluate($expression->clause, $nodes);
-            LogHelper::logMes("$objectClass evaluate $expression->clause: " . ($isOK ? 'true' : 'false'),'', ['category' => 'application-mapper']);
+            LogHelper::logMes("$objectClass evaluate $expression->clause: " . ($isOK ? 'true' : 'false'));
             // these result must be bool and the clause must be a condition sentences
             if (is_bool($isOK) && $isOK) {
                 $result = $expression->result;
                 switch ($expression->result_type) {
                     case ObjectClauseService::TYPE_EXPRESSION:
                         // if result type is a clause use Expression to re-evaluate again to get final result
-                        LogHelper::logMes("$objectClass re-evaluate $expression->result: $result",'', ['category' => 'application-mapper']);
+                        LogHelper::logMes("$objectClass re-evaluate $expression->result: $result");
                         return $expressionLanguage->evaluate($expression->result, $nodes);
                     case ObjectClauseService::TYPE_BOOLEAN:
                         // if result type is a clause use Expression to re-evaluate again to get final result
-                        LogHelper::logMes("$objectClass re-evaluate $expression->result: $result",'', ['category' => 'application-mapper']);
+                        LogHelper::logMes("$objectClass re-evaluate $expression->result: $result");
                         return filter_var($expression->result, FILTER_VALIDATE_BOOL);
                         break;
                     case ObjectClauseService::TYPE_ARRAY:
-                        LogHelper::logMes("$objectClass re-evaluate $expression->result: $result",'', ['category' => 'application-mapper']);
+                        LogHelper::logMes("$objectClass re-evaluate $expression->result: $result");
                         return explode(',', $result);
                 }
-                LogHelper::logMes("$objectClass re-evaluate $expression->result: $result",'', ['category' => 'application-mapper']);
+                LogHelper::logMes("$objectClass re-evaluate $expression->result: $result");
                 return $result;
             }
         }
diff --git a/mrbac/models/forms/ActionForm.php b/mrbac/models/forms/ActionForm.php
index 789fc1e..4a72a7e 100644
--- a/mrbac/models/forms/ActionForm.php
+++ b/mrbac/models/forms/ActionForm.php
@@ -36,11 +36,11 @@ class ActionForm extends Action
             foreach ($modelAction->getPlatformActions() as $plfAction) {
                 $action = ModelService::initAction($plfAction->code, $plfAction->name, $this->description);
                 if ($action->hasErrors()) {
-                    LogHelper::logMes(json_encode($action->errors),'', ['category' => 'application-mrbac']);
+                    LogHelper::logMes(json_encode($action->errors));
                     $this->addError('action', json_encode($action->errors));
                     return false;
                 }
-                LogHelper::logMes("initModelFunction - $modelCode", $code, ['category' => 'application-mrbac']);
+                LogHelper::logMes("initModelFunction - $modelCode", $code);
                 ModelService::initModelFunction($model, $action, strtoupper($this->name), $plfAction->slug);
             }
         }
diff --git a/mrbac/models/forms/ModelForm.php b/mrbac/models/forms/ModelForm.php
index 8eb1480..4fa1ef9 100644
--- a/mrbac/models/forms/ModelForm.php
+++ b/mrbac/models/forms/ModelForm.php
@@ -20,7 +20,7 @@ class ModelForm extends Model
 
     public function save($runValidation = true, $attributeNames = null)
     {
-        LogHelper::logMes(json_encode($this->selection),'', ['category' => 'application-mrbac']);
+        LogHelper::logMes(json_encode($this->selection));
         foreach ($this->selection as $model) {
             list($code, $name) = explode("#", $model);
             ModelService::initModel($code, $name);
diff --git a/mrbac/services/ModelService.php b/mrbac/services/ModelService.php
index 0e02fd6..db7f9e1 100644
--- a/mrbac/services/ModelService.php
+++ b/mrbac/services/ModelService.php
@@ -90,7 +90,7 @@ class ModelService
             $model = new Model(['code' => $code, 'class' => $class]);
         }
         if (!$model->save()) {
-            LogHelper::logMes("init model code $code, class $class errors: " . json_encode($model->getErrors()),'', ['category' => 'application-mrbac']);
+            LogHelper::logMes("init model code $code, class $class errors: " . json_encode($model->getErrors()));
         };
         return $model;
     }
@@ -139,7 +139,7 @@ class ModelService
             'description' => $description,
         ]);
         if (!$model->save()) {
-            LogHelper::logMes("init action code $code, name $name, description $description errors: " . json_encode($model->getErrors()),'', ['category' => 'application-mrbac']);
+            LogHelper::logMes("init action code $code, name $name, description $description errors: " . json_encode($model->getErrors()));
         };
         return $model;
     }
@@ -155,7 +155,7 @@ class ModelService
     {
         $model = Action::findOne(['code' => $code]);
         if (!$model) {
-            LogHelper::logMes("Action $code not found.",'', ['category' => 'application-mrbac']);
+            LogHelper::logMes("Action $code not found.");
             return;
         }
         return $model->delete();
@@ -224,7 +224,7 @@ class ModelService
             'condition' => $expressionCondition,
         ]);
         if (!$condition->save()) {
-            LogHelper::logMes("init model condition code $code, name $expressionCondition, model $model->code errors: " . json_encode($model->getErrors()),'', ['category' => 'application-mrbac']);
+            LogHelper::logMes("init model condition code $code, name $expressionCondition, model $model->code errors: " . json_encode($model->getErrors()));
         };
         return $condition;
     }
@@ -246,7 +246,7 @@ class ModelService
             'expression' => $expression,
         ]);
         if (!$model->save()) {
-            LogHelper::logMes("init model expression code $model->code, expression $expression errors: " . json_encode($model->getErrors()),'', ['category' => 'application-mrbac']);
+            LogHelper::logMes("init model expression code $model->code, expression $expression errors: " . json_encode($model->getErrors()));
         };
         return $model;
     }
@@ -271,7 +271,7 @@ class ModelService
             'slug' => $slug,
         ]);
         if (!$modelFunction->save()) {
-            LogHelper::logMes("init model function 'model_id' => $model->id, 'action_id' => $action->id, 'name' => $name errors: " . json_encode($model->getErrors()),'', ['category' => 'application-mrbac']);
+            LogHelper::logMes("init model function 'model_id' => $model->id, 'action_id' => $action->id, 'name' => $name errors: " . json_encode($model->getErrors()));
         };
         return $modelFunction;
     }
@@ -326,7 +326,7 @@ class ModelService
             'model_id' => $model->id, 'condition_id' => $condition->id, 'function_id' => $modelFunction->id, 'model_code' => $model->code, 'condition' => $condition->condition, 'function_code' => $modelFunction->name
         ]);
         if (!$obj->save()) {
-            LogHelper::logMes("init model Function condition  'model_id' => $model->id, 'condition_id' => $condition->id, 'function_id' => $modelFunction->id errors: " . json_encode($obj->getErrors()),'', ['category' => 'application-mrbac']);
+            LogHelper::logMes("init model Function condition  'model_id' => $model->id, 'condition_id' => $condition->id, 'function_id' => $modelFunction->id errors: " . json_encode($obj->getErrors()));
         };
         return $obj;
     }
diff --git a/workflow/actions/WorkflowRuntimeIndex.php b/workflow/actions/WorkflowRuntimeIndex.php
new file mode 100644
index 0000000..4943741
--- /dev/null
+++ b/workflow/actions/WorkflowRuntimeIndex.php
@@ -0,0 +1,152 @@
+<?php
+
+namespace lsb\workflow\actions;
+
+use lsat\microbase\actions\IndexAction;
+use lsat\microbase\components\ActiveDataProvider;
+use lsb\base\enums\AuthStatusEnum;
+use lsb\base\enums\RecordStatusEnum;
+use lsb\base\services\AuthorizationService;
+use lsb\workflow\models\WorkflowRuntime;
+use lsb\workflow\services\WorkflowRuntimeService;
+use Yii;
+use yii\db\ActiveQuery;
+use yii\db\Expression;
+
+class WorkflowRuntimeIndex extends IndexAction
+{
+
+    public $objectIdAttribute = "id";
+    public $objectInstanceCodeAttribute = "instance_code";
+
+    public $authStatus;
+    public $recordStatus;
+
+    public $status;
+
+    public $assign;
+
+    /**
+     * @inheritdoc
+     */
+    protected function prepareDataProvider()
+    {
+        $requestParams = Yii::$app->getRequest()->getBodyParams();
+        if (empty($requestParams)) {
+            $requestParams = Yii::$app->getRequest()->getQueryParams();
+        }
+        if ($this->prepareFilter !== null) {
+            $requestParams = call_user_func($this->prepareFilter, $requestParams);
+        }
+        $filter = null;
+        if ($this->dataFilter !== null) {
+            $this->dataFilter = Yii::createObject($this->dataFilter);
+            if ($this->dataFilter->load($requestParams)) {
+                $filter = $this->dataFilter->build();
+                if ($filter === false) {
+                    return $this->dataFilter;
+                }
+            }
+        }
+
+        if ($this->prepareDataProvider !== null) {
+            return call_user_func($this->prepareDataProvider, $this, $filter);
+        }
+
+        /* @var $modelClass \yii\db\BaseActiveRecord */
+        $modelClass = $this->modelClass;
+
+        $query = $modelClass::find();
+        if (!empty($filter)) {
+            $query->andWhere($filter);
+        }
+        if (is_callable($this->prepareSearchQuery)) {
+            $query = call_user_func($this->prepareSearchQuery, $query, $requestParams);
+        } else {
+            $query = $this->prepareSearchQuery($query, $requestParams);
+        }
+        $pagination = [
+            'params' => $requestParams,
+        ];
+        if (isset($requestParams['page']) && $requestParams['page'] == -1) {
+            $pagination = false;
+        }
+        return Yii::createObject([
+            'class' => ActiveDataProvider::className(),
+            'query' => $query,
+            'pagination' => $pagination,
+            'sort' => [
+                'params' => $requestParams,
+            ],
+        ]);
+    }
+
+    public function prepareSearchQuery(ActiveQuery $query, $requestParams): callable
+    {
+        $fields = ($this->modelClass)::getKeywordFields();
+        $query = $query->alias('m');
+
+        $runtimeQuery = WorkflowRuntime::find()
+            ->select([
+                'w.object_id'
+            ])->alias('w');
+
+        $runtimeQuery->filterWhere([
+            'w.assign' => AuthorizationService::getUserCode(),
+            'w.auth_status' => AuthStatusEnum::AUTH_STATUS_PROCESSING->value,
+            'w.record_status' => RecordStatusEnum::TYPE_RECORD_STATUS_OPEN->value,
+            'w.status' => 1,
+            'w.object_type' => $this->modelClass,
+        ])
+            ->andWhere(new Expression("w.object_id = m.{$this->objectIdAttribute}"))
+            ->andWhere(new Expression("w.instance_code = m.{$this->objectInstanceCodeAttribute}"));
+        $subquery = $runtimeQuery->createCommand()->rawSql;
+
+        $query->andWhere("EXISTS ($subquery)");
+        if (!$fields || empty($requestParams['keywords'])) {
+            return $query;
+        }
+
+        if ($this->enableFulltextSearch) {
+            $keywordArr = explode(" ", $requestParams['keywords']);
+            return $query->andWhere($this->fulltextSearchField . " @@ to_tsquery(:keywords)", [
+                ":keywords" => "'" . implode("'|'", $keywordArr) . "'"
+            ]);
+        }
+
+        $queryArr = ['or'];
+        foreach ($fields as $field) {
+            $queryArr[] = ['like', $field, $requestParams['keywords']];
+        }
+        return $query->andFilterWhere($queryArr);
+    }
+
+    public function prepareFilter($requestParams): callable
+    {
+        $newRequestParams = $requestParams;
+        if (isset($requestParams['filter'])) {
+            foreach ($requestParams['filter'] as $key => $value) {
+                if (isset($value['ins'])) {
+                    $valueFilter = explode(',', $value['ins']);
+                    if (is_array($valueFilter)) {
+                        foreach ($valueFilter as $item) {
+                            $newRequestParams['filter'][$key]['in'][] = $item;
+                        }
+                    }
+                    unset($newRequestParams['filter'][$key]['ins']);
+                }
+                if (isset($value['nins'])) {
+                    $valueFilter = explode(',', $value['nins']);
+                    if (is_array($valueFilter)) {
+                        foreach ($valueFilter as $item) {
+                            $newRequestParams['filter'][$key]['nin'][] = $item;
+                        }
+                    }
+                    unset($newRequestParams['filter'][$key]['nins']);
+                }
+            }
+        }
+
+        return $newRequestParams;
+    }
+}
diff --git a/workflow/behaviors/InitWorkflowChangeBehavior.php b/workflow/behaviors/InitWorkflowChangeBehavior.php
index a6a1f88..82d386c 100644
--- a/workflow/behaviors/InitWorkflowChangeBehavior.php
+++ b/workflow/behaviors/InitWorkflowChangeBehavior.php
@@ -1,9 +1,9 @@
 <?php
 
 namespace lsb\workflow\behaviors;
-use lsb\base\helper\LogHelper;
 
-use lsb\base\services\AuthorizationService;
+use app\components\LogHelper;
+use app\services\AuthorizationService;
 use lsb\workflow\models\Workflow;
 use lsb\workflow\services\WorkflowConfigService;
 use lsb\workflow\services\WorkflowService;
@@ -42,7 +42,6 @@ class InitWorkflowChangeBehavior extends Behavior
     public $workflow_ref_code;
     public $workflow_version = 1;
     public $instance_code = null;
-    public $silent_error = false;
 
     /**
      * @var string
@@ -51,6 +50,7 @@ class InitWorkflowChangeBehavior extends Behavior
     public $workflow_module = 'workflow';
 
     public $callback = null;
+    public $silent_error = false;
 
     /**
      * Attach events to monitor attribute changes
@@ -92,9 +92,10 @@ class InitWorkflowChangeBehavior extends Behavior
             }
         } catch (\Exception $e) {
             LogHelper::explainException($e);
-            if (!$this->silent_error) {
+            if ($this->silent_error) {
                 throw $e;
             }
         }
+
     }
-}
\ No newline at end of file
+}
diff --git a/workflow/command/WorkflowController.php b/workflow/command/WorkflowController.php
new file mode 100644
index 0000000..3795aa1
--- /dev/null
+++ b/workflow/command/WorkflowController.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace lsb\workflow\command;
+
+use app\components\LogHelper;
+use lsat\microbase\models\User;
+use lsb\workflow\services\WorkflowConfigService;
+use yii\console\Controller;
+
+class WorkflowController extends Controller
+{
+    public function actionInit($workflowCode, $refCode, $tenantCode = 'SPVB')
+    {
+        \Yii::$app->user->setIdentity(new User([
+            'tenantCode' => $tenantCode,
+        ]));
+        $workflowCurrentCode = WorkflowConfigService::getWorkflowCode($refCode);
+        switch ($workflowCode) {
+            case $workflowCurrentCode:
+                LogHelper::logMes("Workflow $workflowCode - $refCode has been inited");
+                return;
+            default:
+                WorkflowConfigService::createOrMapping($workflowCode, $refCode);
+        }
+    }
+}
diff --git a/workflow/services/WorkflowConfigService.php b/workflow/services/WorkflowConfigService.php
index 381d032..91d5025 100644
--- a/workflow/services/WorkflowConfigService.php
+++ b/workflow/services/WorkflowConfigService.php
@@ -35,7 +35,7 @@ class WorkflowConfigService
         if (!$workflowConfig) {
             $workflowConfig = new WorkflowConfig(['workflow_code' => $workflowCode]);
             if (!$workflowConfig->save()) {
-                LogHelper::logMes("save WorkflowConfig errors: " . json_encode($workflowConfig->errors),'', ['category' => 'application-worlflow']);
+                LogHelper::logMes("save WorkflowConfig errors: " . json_encode($workflowConfig->errors));
                 $transaction->rollBack();
                 return;
             }
@@ -44,7 +44,7 @@ class WorkflowConfigService
         $workflowConfigReference = new WorkflowConfigReference(['workflow_config_id' => $workflowConfig->id, 'ref_code' => $refCode]);
 
         if (!$workflowConfigReference->save()) {
-            LogHelper::logMes("save WorkflowConfigReference errors: " . json_encode($workflowConfigReference->errors),'', ['category' => 'application-worlflow']);
+            LogHelper::logMes("save WorkflowConfigReference errors: " . json_encode($workflowConfigReference->errors));
             $transaction->rollBack();
             return;
         }
diff --git a/workflow/services/WorkflowRuntimeService.php b/workflow/services/WorkflowRuntimeService.php
index 02feed3..c3e369d 100644
--- a/workflow/services/WorkflowRuntimeService.php
+++ b/workflow/services/WorkflowRuntimeService.php
@@ -2,7 +2,10 @@
 
 namespace lsb\workflow\services;
 
+use app\models\tstb\Transaction;
+use app\services\AuthorizationService;
 use lsb\base\enums\AuthStatusEnum;
+use lsb\base\enums\RecordStatusEnum;
 use lsb\workflow\models\Workflow;
 use lsb\workflow\models\WorkflowRuntime;
 use yii\web\NotFoundHttpException;
@@ -37,6 +40,7 @@ class WorkflowRuntimeService
     {
         return WorkflowRuntime::findOne(['runtime_id' => $runtimeId, 'object_type' => $objectType]);
     }
+
     public static function cleanWorkflowRuntime($objectId, $objectType)
     {
         return WorkflowRuntime::deleteAll(['object_id' => $objectId, 'object_type' => $objectType]);
@@ -46,7 +50,7 @@ class WorkflowRuntimeService
     {
         $runtime = WorkflowRuntimeService::getRuntimeByUserCode($instanceCode, $userCode, $objectId, $objectType);
         if (!$runtime) {
-            throw new NotFoundHttpException("self handle runtume not found: $instanceCode, $userCode, $objectId, $objectType ");
+            throw new NotFoundHttpException("self handle runtime not found: $instanceCode, $userCode, $objectId, $objectType ");
         }
         $results = WorkflowService::getAllowActions($workflow, $runtime->runtime_id, $params, $token);
         if (!$results['status']) {
@@ -54,10 +58,24 @@ class WorkflowRuntimeService
         }
         $action = array_shift($results['data']);
 
-        $result =  WorkflowService::executeAction($workflow, $runtime->runtime_id, $action['code'], $message, $token);
+        $result = WorkflowService::executeAction($workflow, $runtime->runtime_id, $action['code'], $message, $token);
         WorkflowService::saveRuntime($workflow, $instanceCode, $workflowCode, $objectId, $objectType, $token);
-        if($callback && $callback instanceof \Closure){
+        if ($callback && $callback instanceof \Closure) {
             call_user_func_array($callback, ['result' => $result, 'runtime' => $runtime]);
         }
     }
-}
\ No newline at end of file
+
+    public static function getPendingWorkflowRuntime($objectId, $instanceCode, $userCode, $objectType)
+    {
+        return WorkflowRuntime::find()->where([
+            'object_id' => $objectId,
+            'instance_code' => $instanceCode,
+            'assign' => $userCode,
+            'auth_status' => AuthStatusEnum::AUTH_STATUS_PROCESSING,
+            'status' => 1,
+            'record_status' => RecordStatusEnum::TYPE_RECORD_STATUS_OPEN
+        ])->andFilterWhere([
+            'object_type' => $objectType,
+        ]);
+    }
+}
diff --git a/workflow/services/WorkflowService.php b/workflow/services/WorkflowService.php
index 2770034..39b1f97 100644
--- a/workflow/services/WorkflowService.php
+++ b/workflow/services/WorkflowService.php
@@ -21,7 +21,7 @@ class WorkflowService
 {
     public static function createWorkflow(Workflow $workflow, $instanceCode, $workflowCode, $token)
     {
-        LogHelper::logMes("createWorkflow: $workflowCode - $instanceCode ",'', ['category' => 'application-worlflow']);
+        LogHelper::logMes("createWorkflow: $workflowCode - $instanceCode ");
         $result = self::getBaseService($workflow->baseUrl)
             ->addAuthenticator(new HttpBearer($token))
             ->post(ApiEndpoint::WORKFLOW_API->getEndPoint('/', 'create-workflow-runtime'), [
@@ -36,7 +36,7 @@ class WorkflowService
 
     public static function startWorkflow(Workflow $workflow, $instanceCode, $token, $version = 1)
     {
-        LogHelper::logMes("startWorkflow: $instanceCode",'', ['category' => 'application-worlflow']);
+        LogHelper::logMes("startWorkflow: $instanceCode");
         $result = self::getBaseService($workflow->baseUrl)
             ->addAuthenticator(new HttpBearer($token))
             ->post(ApiEndpoint::WORKFLOW_API->getEndPoint('/', 'approve-workflow-runtime'), [
@@ -45,13 +45,13 @@ class WorkflowService
             ]);
 
         $result = json_decode($result->getContents(), true);
-        LogHelper::logMes("startWorkflow: $instanceCode" . json_encode($result),'', ['category' => 'application-worlflow']);
+        LogHelper::logMes("startWorkflow: $instanceCode" . json_encode($result));
         return $result;
     }
 
     public static function getRuntimes(Workflow $workflow, $instanceCode, $workflowCode, $token)
     {
-        LogHelper::logMes("getRuntimes: $instanceCode - $workflowCode ",'', ['category' => 'application-worlflow']);
+        LogHelper::logMes("getRuntimes: $instanceCode - $workflowCode ");
         $result = self::getBaseService($workflow->baseUrl)
             ->addAuthenticator(new HttpBearer($token))
             ->post(ApiEndpoint::WORKFLOW_API->getEndPoint('/', 'wf-engine-runtime-version'), [
@@ -60,13 +60,13 @@ class WorkflowService
             ]);
 
         $result = json_decode($result->getContents(), true);
-        LogHelper::logMes("getRuntimes: $instanceCode - $workflowCode " . json_encode($result),'', ['category' => 'application-worlflow']);
+        LogHelper::logMes("getRuntimes: $instanceCode - $workflowCode " . json_encode($result));
         return $result;
     }
 
     public static function getAllowActions(Workflow $workflow, $runtimeId, $params, $token)
     {
-        LogHelper::logMes("getAllowActions: $runtimeId ",'', ['category' => 'application-worlflow']);
+        LogHelper::logMes("getAllowActions: $runtimeId ");
         $result = self::getBaseService($workflow->baseUrl)
             ->addAuthenticator(new HttpBearer($token))
             ->post(ApiEndpoint::WORKFLOW_API->getEndPoint('/', 'wf-engine-get-allowed-actions'), [
@@ -75,22 +75,23 @@ class WorkflowService
             ]);
 
         $result = json_decode($result->getContents(), true);
-        LogHelper::logMes("getAllowActions: $runtimeId" . json_encode($result),'', ['category' => 'application-worlflow']);
+        LogHelper::logMes("getAllowActions: $runtimeId" . json_encode($result));
         return $result;
     }
 
-    public static function executeAction(Workflow $workflow, $runtimeId, $codeAction, $description, $token)
+    public static function executeAction(Workflow $workflow, $runtimeId, $codeAction, $description, $token, $extend = [])
     {
-        LogHelper::logMes("executeAction: $runtimeId - $codeAction - $description ", $runtimeId, ['category' => 'application-worlflow']);
+        LogHelper::logMes("executeAction: $runtimeId - $codeAction - $description ", $runtimeId);
         $result = self::getBaseService($workflow->baseUrl)
             ->addAuthenticator(new HttpBearer($token))
             ->post(ApiEndpoint::WORKFLOW_API->getEndPoint('/', 'execute-action'), [
                 'runtimeID' => $runtimeId,
                 'codeAction' => $codeAction,
                 'description' => $description,
+                "extent" => $extend
             ]);
         $result = json_decode($result->getContents(), true);
-        LogHelper::logMes("executeAction: $runtimeId - $codeAction - $description " . json_encode($result), $runtimeId,  ['category' => 'application-worlflow']);
+        LogHelper::logMes("executeAction: $runtimeId - $codeAction - $description " . json_encode($result), $runtimeId);
         return $result;
     }
 
@@ -108,8 +109,8 @@ class WorkflowService
         $runtimes = $runtimes['data'] ?? [];
         $versions = array_keys($runtimes);
         foreach ($versions as $version) {
-            $runtime_versions = $runtimes[$version] ?? [];
-            if(empty($runtime_versions) || !is_integer($runtime_versions)){
+            $runtime_versions = $runtimes[$version];
+            if(empty($runtime_versions)){
                 LogHelper::logMes("errors: no runtime version found" ,'', ['category' => 'application-worlflow']);
                 continue;
             }
@@ -150,7 +151,7 @@ class WorkflowService
                 ]);
                 $workflow_runtime->detachBehaviors();
                 if (!$workflow_runtime->save()) {
-                    LogHelper::logMes("errors: " . json_encode($workflow_runtime->getErrors()),'', ['category' => 'application-worlflow']);
+                    LogHelper::logMes("errors: " . json_encode($workflow_runtime->getErrors()));
                 }
             }
         }
@@ -191,5 +192,4 @@ class WorkflowService
         }
         return BaseService::getInstance($url, $auth, ContentType::CONTENT_TYPE_APPLICATION_JSON);
     }
-
-}
\ No newline at end of file
+}
diff --git a/workflow/traits/WorkflowRuntimeTrait.php b/workflow/traits/WorkflowRuntimeTrait.php
new file mode 100644
index 0000000..e9d447c
--- /dev/null
+++ b/workflow/traits/WorkflowRuntimeTrait.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace lsb\workflow\traits;
+
+use lsb\base\services\AuthorizationService;
+use lsb\workflow\services\WorkflowRuntimeService;
+
+trait WorkflowRuntimeTrait
+{
+    public function getWorkflowRuntime()
+    {
+        return WorkflowRuntimeService::getPendingWorkflowRuntime($this->id, $this->instance_code, AuthorizationService::getUserCode(), get_class($this));
+    }
+}
-- 
GitLab