diff --git a/resources/lang/en/exment.php b/resources/lang/en/exment.php index 205fc42fc..75e283986 100644 --- a/resources/lang/en/exment.php +++ b/resources/lang/en/exment.php @@ -275,6 +275,7 @@ 'outside_api' => 'Perform server external communication', 'permission_available' => 'Use Role Management', 'organization_available' => 'Use Organization Management', + 'logging_toggle_available' => 'Enable Operation Log', 'system_admin_users' => 'System Administrator', 'system_mail' => 'System Mail Settings', 'system_mail_host' => 'Host Name', @@ -447,6 +448,7 @@ 'datalist_pager_count' => 'It is the default number of display of data that is displayed in keyword search and data list of dashboard. It is reflected in the whole system.', 'permission_available' => 'If Select YES, management role using user or organozation.', 'organization_available' => 'If Select YES, create organizations to which the user belongs.', + 'logging_toggle_available' => 'If set to YES, the operation log feature will be enabled and all user actions will be recorded.', 'system_admin_users' => 'Set the user who will be the system administrator.', 'system_mail' => 'Configure settings when sending mail from the system.', 'system_mail_from' => 'the mail address from this system. Using this mail address as "from", this system sends users.', diff --git a/resources/lang/ja/exment.php b/resources/lang/ja/exment.php index 07cc6ffe6..716158b22 100644 --- a/resources/lang/ja/exment.php +++ b/resources/lang/ja/exment.php @@ -275,6 +275,7 @@ 'outside_api' => 'サーバー外部通信を行う', 'permission_available' => '権限管理を使用する', 'organization_available' => '組織管理を使用する', + 'logging_toggle_available' => '操作ログを有効にする', 'system_admin_users' => 'システム管理者', 'system_mail' => 'システムメール設定', 'system_mail_host' => 'ホスト名', @@ -447,6 +448,7 @@ 'datalist_pager_count' => 'キーワード検索や、ダッシュボードのデータ一覧で表示されるデータの、既定の表示件数です。システム全体に反映されます。', 'permission_available' => 'YESにした場合、ユーザーや役割によって、アクセスできる項目を管理します。', 'organization_available' => 'YESにした場合、ユーザーが所属する組織や部署を作成します。', + 'logging_toggle_available' => 'YESに設定すると、操作ログ機能が有効になり、すべてのユーザーの操作が記録されます。', 'system_admin_users' => 'システム管理者となるユーザーを設定してください。', 'system_mail' => 'システムからメールを送付する時の設定を行います。', 'system_mail_from' => '送信元のメールアドレスです。このメールアドレスをFromとして、メールが送付されます。', diff --git a/src/Console/UpdateCommand.php b/src/Console/UpdateCommand.php index 1868d8e72..d6780d79e 100644 --- a/src/Console/UpdateCommand.php +++ b/src/Console/UpdateCommand.php @@ -2,6 +2,8 @@ namespace Exceedone\Exment\Console; +use Exceedone\Exment\Enums\SystemTableName; +use Exceedone\Exment\Model\CustomTable; use Illuminate\Console\Command; use Exceedone\Exment\Services\TemplateImportExport\TemplateImporter; @@ -78,5 +80,11 @@ public function initDatabase() // Remove template import if update // $importer = new TemplateImporter; // $importer->importSystemTemplate(true); + $customTable = CustomTable::where('table_name', SystemTableName::SYSTEM_LOGS)->first(); + if (!$customTable) { + $importer = new TemplateImporter(); + $importer->importSystemLogsTemplate(); + } + } } diff --git a/src/Enums/SystemTableName.php b/src/Enums/SystemTableName.php index 9c3b486f4..d5f238cf7 100644 --- a/src/Enums/SystemTableName.php +++ b/src/Enums/SystemTableName.php @@ -10,6 +10,7 @@ class SystemTableName extends EnumBase { public const SYSTEM = 'systems'; + public const SYSTEM_LOGS = 'system_logs'; public const LOGIN_USER = 'login_users'; public const PLUGIN = 'plugins'; public const USER = 'user'; diff --git a/src/ExmentServiceProvider.php b/src/ExmentServiceProvider.php index e67c3459d..068eb007e 100644 --- a/src/ExmentServiceProvider.php +++ b/src/ExmentServiceProvider.php @@ -103,6 +103,7 @@ class ExmentServiceProvider extends ServiceProvider */ protected $middleware = [ \Exceedone\Exment\Middleware\TrustProxies::class, + // \Exceedone\Exment\Middleware\LogRouteExecutionTime::class, \Exceedone\Exment\Middleware\ExmentDebug::class, ]; @@ -114,6 +115,8 @@ class ExmentServiceProvider extends ServiceProvider */ protected $routeMiddleware = [ 'admin.auth' => \Exceedone\Exment\Middleware\Authenticate::class, + 'log.exec.time' => \Exceedone\Exment\Middleware\LogRouteExecutionTime::class, + 'check.logging.enabled' => \Exceedone\Exment\Middleware\CheckLoggingEnabled::class, 'admin.auth-2factor' => \Exceedone\Exment\Middleware\Authenticate2factor::class, 'admin.password-limit' => \Exceedone\Exment\Middleware\AuthenticatePasswordLimit::class, 'admin.bootstrap2' => \Exceedone\Exment\Middleware\Bootstrap::class, @@ -154,6 +157,8 @@ class ExmentServiceProvider extends ServiceProvider 'admin.web-ipfilter', 'admin.initialize', 'admin.auth', + 'log.exec.time', + 'check.logging.enabled', 'admin.auth-2factor', 'admin.password-limit', 'admin.morph', diff --git a/src/Middleware/CheckLoggingEnabled.php b/src/Middleware/CheckLoggingEnabled.php new file mode 100644 index 000000000..7275f4282 --- /dev/null +++ b/src/Middleware/CheckLoggingEnabled.php @@ -0,0 +1,49 @@ +base_user; + $email = $user->getValue('email'); + $url = $request->fullUrl(); + $date_time = Carbon::now()->toDateTimeString(); + $system_logs = $table->getValueModel(); + $system_logs->parent_id = null; + $system_logs->parent_type = null; + $system_logs->setValue('email', $email); + $system_logs->setValue('create_at', $date_time); + $system_logs->setValue('url', $url); + $allSql = implode("\n", QueryLogger::all()); + $system_logs->setValue('sql', $allSql); + $system_logs->save(); + QueryLogger::clear(); + } + } + + return $response; + } +} diff --git a/src/Middleware/ExmentDebug.php b/src/Middleware/ExmentDebug.php index a2fa5e221..6c2aefb95 100644 --- a/src/Middleware/ExmentDebug.php +++ b/src/Middleware/ExmentDebug.php @@ -5,6 +5,9 @@ use Illuminate\Http\Request; use Exceedone\Exment\Enums\EnumBase; use Illuminate\Console\Scheduling\Schedule; +use Exceedone\Exment\Model\CustomTable; +use Exceedone\Exment\Model\System; +use Exceedone\Exment\Services\QueryLogger; class ExmentDebug { @@ -18,7 +21,7 @@ public function handle(Request $request, \Closure $next) public static function handleLog(?Request $request = null) { - if (boolval(config('exment.debugmode', false)) || boolval(config('exment.debugmode_sql', false))) { + if (boolval(config('exment.debugmode', false)) || boolval(config('exment.debugmode_sql', false)) || System::logging_toggle_available()) { static::logDatabase(); } @@ -27,6 +30,7 @@ public static function handleLog(?Request $request = null) } } + protected static $queryLogs = []; /** * Output log database @@ -46,7 +50,8 @@ protected static function logDatabase() $sql = preg_replace("/\?/", "'{$binding}'", $sql, 1); } - $log_string = "TIME:{$query->time}ms; SQL: $sql"; + $log_string = "TIME:{$query->time}ms SQL: $sql"; + QueryLogger::add($log_string); if (boolval(config('exment.debugmode_sqlfunction', false))) { $function = static::getFunctionName(); $log_string .= "; function: $function"; diff --git a/src/Middleware/LogRouteExecutionTime.php b/src/Middleware/LogRouteExecutionTime.php new file mode 100644 index 000000000..f3ba750ad --- /dev/null +++ b/src/Middleware/LogRouteExecutionTime.php @@ -0,0 +1,49 @@ +fullUrl()); + $start = microtime(true); + } + $response = $next($request); + if ($log_enabled) { + $user = \Exment::user()->base_user; + $email = $user->getValue('email'); + $url = $request->fullUrl(); + $date_time = Carbon::now()->toDateTimeString(); + $execution_logs = CustomTable::getEloquent('execution_logs')->getValueModel(); + $execution_logs->parent_id = null; + $execution_logs->parent_type = null; + $execution_logs->setValue('email', $email); + $execution_logs->setValue('create_at', $date_time); + $execution_logs->setValue('url', $url); + $allSql = implode("\n", QueryLogger::all()); + $execution_logs->setValue('sql', $allSql); + $execution_logs->save(); + QueryLogger::clear(); + } + + return $response; + } +} diff --git a/src/Model/Define.php b/src/Model/Define.php index f5873bb6e..ba353090a 100644 --- a/src/Model/Define.php +++ b/src/Model/Define.php @@ -56,6 +56,7 @@ class Define 'outside_api' => ['type' => 'boolean', 'group' => 'initialize', 'default' => true], 'permission_available' => ['type' => 'boolean', 'default' => '1', 'group' => 'initialize'], 'organization_available' => ['type' => 'boolean', 'default' => '1', 'group' => 'initialize'], + 'logging_toggle_available' => ['type' => 'boolean', 'default' => '0', 'group' => 'initialize'], // Advanced ---------------------------------- 'filter_search_type' => ['default' => 'forward', 'group' => 'advanced'], diff --git a/src/Model/System.php b/src/Model/System.php index 885cc9670..489eb810c 100644 --- a/src/Model/System.php +++ b/src/Model/System.php @@ -25,6 +25,7 @@ * @method static boolean|void outside_api($arg = null) * @method static boolean|void permission_available($arg = null) * @method static boolean|void organization_available($arg = null) + * @method static boolean|void logging_toggle_available($arg = null) * @method static string|void filter_search_type($arg = null) * @method static string|void system_mail_host($arg = null) * @method static string|void system_mail_port($arg = null) diff --git a/src/Services/Installer/InitializeFormTrait.php b/src/Services/Installer/InitializeFormTrait.php index f0d79bac0..bb4ad0977 100644 --- a/src/Services/Installer/InitializeFormTrait.php +++ b/src/Services/Installer/InitializeFormTrait.php @@ -99,6 +99,9 @@ protected function getInitializeForm($routeName, $isInitialize = false): WidgetF $form->switchbool('organization_available', exmtrans("system.organization_available")) ->help(exmtrans("system.help.organization_available")); + $form->switchbool('logging_toggle_available', exmtrans("system.logging_toggle_available")) + ->help(exmtrans("system.help.logging_toggle_available")); + // template list if ($isInitialize) { $this->addTemplateTile($form); diff --git a/src/Services/QueryLogger.php b/src/Services/QueryLogger.php new file mode 100644 index 000000000..cbec98bc5 --- /dev/null +++ b/src/Services/QueryLogger.php @@ -0,0 +1,24 @@ +importFromFile(File::get($path), [ + 'system_flg' => true, + 'is_update' => $is_update, + 'basePath' => $templates_base_path, + ]); + } + /** * Upload template and import (from display) */ diff --git a/templates/system_logs/config.json b/templates/system_logs/config.json new file mode 100644 index 000000000..1293b214d --- /dev/null +++ b/templates/system_logs/config.json @@ -0,0 +1,325 @@ +{ + "template_name": "system_logs", + "version": "dev-feature\/add-log-url-debugmode", + "template_view_name": "System Logs", + "description": null, + "custom_tables": [ + { + "table_name": "system_logs", + "table_view_name": "システムログ", + "description": null, + "system_flg": "0", + "showlist_flg": "1", + "order": "0", + "options": { + "icon": "fa-pencil", + "search_enabled": "1", + "use_label_id_flg": "0", + "one_record_flg": "0", + "attachment_flg": "0", + "comment_flg": "0", + "revision_flg": "0", + "data_submit_redirect": "inherit", + "all_user_editable_flg": "0", + "all_user_viewable_flg": "0", + "all_user_accessable_flg": "0", + "revision_count": "100" + }, + "custom_columns": [ + { + "column_name": "url", + "column_view_name": "実行URL", + "column_type": "url", + "description": null, + "system_flg": "0", + "order": "0", + "options": { + "required": "0", + "index_enabled": "0", + "freeword_search": "0", + "unique": "0", + "init_only": "0", + "select_import_table_name": null, + "select_import_column_name": null, + "select_export_table_name": null, + "select_export_column_name": null + } + }, + { + "column_name": "email", + "column_view_name": "実行ユーザ", + "column_type": "text", + "description": null, + "system_flg": "0", + "order": "0", + "options": { + "required": "0", + "index_enabled": "0", + "freeword_search": "0", + "unique": "0", + "init_only": "0", + "string_length": "256", + "available_characters": [], + "suggest_input": "0", + "select_import_table_name": null, + "select_import_column_name": null, + "select_export_table_name": null, + "select_export_column_name": null + } + }, + { + "column_name": "create_at", + "column_view_name": "実行時間", + "column_type": "datetime", + "description": null, + "system_flg": "0", + "order": "0", + "options": { + "required": "0", + "index_enabled": "0", + "freeword_search": "0", + "unique": "0", + "init_only": "0", + "datetime_now_saving": "0", + "datetime_now_creating": "0", + "select_import_table_name": null, + "select_import_column_name": null, + "select_export_table_name": null, + "select_export_column_name": null + } + }, + { + "column_name": "sql", + "column_view_name": "SQL", + "column_type": "textarea", + "description": null, + "system_flg": "0", + "order": "0", + "options": { + "required": "0", + "index_enabled": "0", + "freeword_search": "0", + "unique": "0", + "init_only": "0", + "string_length": "256", + "rows": "6", + "select_import_table_name": null, + "select_import_column_name": null, + "select_export_table_name": null, + "select_export_column_name": null + } + } + ], + "custom_column_multisettings": [] + } + ], + "custom_relations": [], + "custom_forms": [ + { + "suuid": "5160ffbedcfce843fb8e", + "form_view_name": "フォーム", + "default_flg": "1", + "options": { + "form_label_type": "horizontal", + "show_grid_type": "grid" + }, + "custom_form_blocks": [ + { + "form_block_view_name": null, + "form_block_type": "0", + "available": "1", + "options": [], + "custom_form_columns": [ + { + "suuid": "8b556da035451e99e2ed", + "form_column_type": "99", + "row_no": "1", + "column_no": "1", + "width": "1", + "options": { + "html": "項要<\/span>\r\n", + "changedata_column_table_name": null, + "changedata_column_name": null, + "changedata_target_table_name": null, + "changedata_target_column_name": null, + "relation_filter_target_table_name": null, + "relation_filter_target_column_name": null + }, + "order": "1", + "form_column_target_name": "html" + }, + { + "suuid": "69778ed997424cd8655c", + "form_column_type": "0", + "row_no": "2", + "column_no": "0", + "width": "1", + "options": { + "changedata_column_table_name": null, + "changedata_column_name": null, + "changedata_target_table_name": null, + "changedata_target_column_name": null, + "relation_filter_target_table_name": null, + "relation_filter_target_column_name": null + }, + "order": "2", + "form_column_target_name": "create_at" + }, + { + "suuid": "0d4b254b9257a4a4ab01", + "form_column_type": "0", + "row_no": "2", + "column_no": "1", + "width": "1", + "options": { + "changedata_column_table_name": null, + "changedata_column_name": null, + "changedata_target_table_name": null, + "changedata_target_column_name": null, + "relation_filter_target_table_name": null, + "relation_filter_target_column_name": null + }, + "order": "3", + "form_column_target_name": "email" + }, + { + "suuid": "6108719e3fc8cd7f07ed", + "form_column_type": "0", + "row_no": "2", + "column_no": "2", + "width": "1", + "options": { + "changedata_column_table_name": null, + "changedata_column_name": null, + "changedata_target_table_name": null, + "changedata_target_column_name": null, + "relation_filter_target_table_name": null, + "relation_filter_target_column_name": null + }, + "order": "4", + "form_column_target_name": "url" + }, + { + "suuid": "05d58904afdbfb98064e", + "form_column_type": "99", + "row_no": "3", + "column_no": "1", + "width": "3", + "options": { + "html": "詳細<\/span>\r\n", + "changedata_column_table_name": null, + "changedata_column_name": null, + "changedata_target_table_name": null, + "changedata_target_column_name": null, + "relation_filter_target_table_name": null, + "relation_filter_target_column_name": null + }, + "order": "5", + "form_column_target_name": "html" + }, + { + "suuid": "d8ac71775daaa57988f1", + "form_column_type": "0", + "row_no": "3", + "column_no": "1", + "width": "3", + "options": { + "changedata_column_table_name": null, + "changedata_column_name": null, + "changedata_target_table_name": null, + "changedata_target_column_name": null, + "relation_filter_target_table_name": null, + "relation_filter_target_column_name": null + }, + "order": "6", + "form_column_target_name": "sql" + } + ], + "form_block_target_table_name": "system_logs" + } + ], + "table_name": "system_logs" + } + ], + "custom_views": [ + { + "suuid": "d9ce8f6d3b288d6da84b", + "view_type": "0", + "view_kind_type": "9", + "view_view_name": "全件ビュー", + "default_flg": "1", + "order": "0", + "options": { + "pager_count": "0", + "use_view_infobox": "0" + }, + "custom_options": null, + "custom_view_columns": [ + { + "suuid": "b72cbe1340891325f453", + "view_column_type": "1", + "order": "10", + "options": [], + "view_column_name": "実行時間", + "view_column_color": null, + "view_column_font_color": null, + "sort_order": null, + "sort_type": null, + "view_column_table_name": "system_logs", + "view_column_target_name": "created_at", + "view_pivot_table_name": null, + "view_pivot_column_name": null + }, + { + "suuid": "5f487b7c30057e5f9705", + "view_column_type": "0", + "order": "20", + "options": [], + "view_column_name": null, + "view_column_color": null, + "view_column_font_color": null, + "sort_order": null, + "sort_type": null, + "view_column_table_name": "system_logs", + "view_column_target_name": "email", + "view_pivot_table_name": null, + "view_pivot_column_name": null + }, + { + "suuid": "90a9dcf0b135b3d330b8", + "view_column_type": "0", + "order": "30", + "options": [], + "view_column_name": null, + "view_column_color": null, + "view_column_font_color": null, + "sort_order": null, + "sort_type": null, + "view_column_table_name": "system_logs", + "view_column_target_name": "url", + "view_pivot_table_name": null, + "view_pivot_column_name": null + } + ], + "custom_view_filters": [], + "custom_view_sorts": [], + "custom_view_summaries": [], + "custom_view_grid_filters": [], + "table_name": "system_logs" + } + ], + "custom_copies": [], + "admin_menu": [ + { + "order": "4", + "title": "システムログ", + "icon": "fa-brands fa-blogger", + "uri": "system_logs", + "options": [], + "menu_type": "table", + "menu_name": "system_logs", + "parent_name": null, + "menu_target_name": "system_logs" + } + ] +} \ No newline at end of file diff --git a/tests/Browser/ExmentKitPrepareTrait.php b/tests/Browser/ExmentKitPrepareTrait.php index d9d7d2d8d..190c2a444 100644 --- a/tests/Browser/ExmentKitPrepareTrait.php +++ b/tests/Browser/ExmentKitPrepareTrait.php @@ -25,7 +25,7 @@ protected function createCustomRelation($parent_table, $child_table, $relation_t // Create custom relation $this->visit(admin_url("relation/$parent_table/create")) ->submitForm('admin-submit', $data) - ->seePageIs('/admin/relation/' . $parent_table) + ->seePageIs(admin_url('relation/' . $parent_table)) ->seeInElement('td', array_get($row, 'table_view_name')); } @@ -246,7 +246,7 @@ protected function createCustomColumns($table_name, $targets = null) // Create custom column $this->post(admin_url("column/$table_name"), $data); $this->visit(admin_url("column/$table_name")) - ->seePageIs("/admin/column/$table_name") + ->seePageIs(admin_url("column/$table_name")) ->seeInElement('td', array_get($data, 'column_view_name')); } } diff --git a/tests/Browser/ExmentKitTestCase.php b/tests/Browser/ExmentKitTestCase.php index cd7141555..1746e2c45 100644 --- a/tests/Browser/ExmentKitTestCase.php +++ b/tests/Browser/ExmentKitTestCase.php @@ -59,7 +59,30 @@ protected function setUpTraits() */ protected function login($id = null) { - $this->be(LoginUser::find($id?? 1)); + $targetId = $id ?? 1; + $user = LoginUser::find($targetId); + + if (!$user) { + // Try to create a minimal test user if it doesn't exist + try { + $this->createTestUserIfNeeded($targetId); + $user = LoginUser::find($targetId); + } catch (\Exception $e) { + // If we still can't find or create the user, throw an informative error + throw new \RuntimeException( + "Test user with ID " . $targetId . " not found and could not be created. " . + "Please ensure test data is properly seeded. Error: " . $e->getMessage() + ); + } + } + + if (!$user) { + throw new \RuntimeException( + "Test user with ID " . $targetId . " not found. Please ensure test data is properly seeded." + ); + } + + $this->be($user); } @@ -114,4 +137,26 @@ public function containsSelectOptions($element, array $options, $negate = false) { return $this->assertInPage(new Constraints\ContainsSelectOption($element, $options), $negate); } + /** + * Create a minimal test user if needed + * @param int $id + * @return void + */ + private function createTestUserIfNeeded($id) + { + // Check if we have basic custom tables + $userTable = \Exceedone\Exment\Model\CustomTable::getEloquent('user'); + if (!$userTable) { + // Try to seed again + \Artisan::call('db:seed', [ + '--class' => 'Exceedone\\Exment\\Database\\Seeder\\InstallSeeder', + '--force' => true + ]); + \Artisan::call('db:seed', [ + '--class' => 'Exceedone\\Exment\\Database\\Seeder\\TestDataSeeder', + '--force' => true + ]); + } + } + }