From a10908e22788fa77253891ffc5378c846340d631 Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Sat, 28 Feb 2026 04:42:58 +0530 Subject: [PATCH] Add one-time DB setup utility for migration scaffolding and lock --- app/Config/Routes.php | 4 + app/Controllers/Setup.php | 219 +++++++++++++++++++++++++++++++++++ app/Views/setup/db_setup.php | 72 ++++++++++++ 3 files changed, 295 insertions(+) create mode 100644 app/Controllers/Setup.php create mode 100644 app/Views/setup/db_setup.php diff --git a/app/Config/Routes.php b/app/Config/Routes.php index bee718c..eb3cbb5 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -223,6 +223,10 @@ $routes->post('Payment_Medical/change_user', 'Payment_Medical::change_user'); service('auth')->routes($routes); +$routes->get('setup/db-tools', 'Setup::dbTools'); +$routes->post('setup/db-tools/generate', 'Setup::generateMigrations'); +$routes->post('setup/db-tools/complete', 'Setup::complete'); + $routes->group('billing', function($routes) { $routes->get('ipd', 'Billing\\Ipd::index'); diff --git a/app/Controllers/Setup.php b/app/Controllers/Setup.php new file mode 100644 index 0000000..e37a1c2 --- /dev/null +++ b/app/Controllers/Setup.php @@ -0,0 +1,219 @@ +lockFilePath()); + } + + private function ensureSetupAccess() + { + if ($this->isSetupLocked()) { + return $this->response->setStatusCode(404)->setBody('Setup is already completed and locked.'); + } + + if (!function_exists('auth')) { + return null; + } + + $user = auth()->user(); + if (!$user) { + return $this->response->setStatusCode(403)->setBody('Please login to access setup tools.'); + } + + $allowed = false; + if (method_exists($user, 'can') && $user->can('billing.opd.edit')) { + $allowed = true; + } + if (!$allowed && method_exists($user, 'inGroup') && $user->inGroup('admin', 'superadmin', 'developer')) { + $allowed = true; + } + + if (!$allowed) { + return $this->response->setStatusCode(403)->setBody('Access denied.'); + } + + return null; + } + + public function dbTools() + { + if ($deny = $this->ensureSetupAccess()) { + return $deny; + } + + $allTables = $this->db->listTables(); + $scripted = $this->detectScriptedTables(); + + $tableRows = []; + foreach ($allTables as $tableName) { + $isScripted = isset($scripted[strtolower((string) $tableName)]); + $tableRows[] = [ + 'name' => (string) $tableName, + 'scripted' => $isScripted, + ]; + } + + usort($tableRows, static function (array $a, array $b): int { + if ($a['scripted'] === $b['scripted']) { + return strcmp($a['name'], $b['name']); + } + return $a['scripted'] ? 1 : -1; + }); + + $flash = session('setup_msg'); + + return view('setup/db_setup', [ + 'tables' => $tableRows, + 'msg' => $flash, + 'lock_file' => $this->lockFilePath(), + ]); + } + + public function generateMigrations() + { + if ($deny = $this->ensureSetupAccess()) { + return $deny; + } + + $selected = $this->request->getPost('tables'); + if (!is_array($selected)) { + $selected = []; + } + + $selected = array_values(array_unique(array_map(static function ($value): string { + $name = strtolower(trim((string) $value)); + return preg_replace('/[^a-z0-9_]/', '', $name) ?? ''; + }, $selected))); + $selected = array_values(array_filter($selected)); + + if (empty($selected)) { + session()->setFlashdata('setup_msg', 'Please select at least one table.'); + return redirect()->to(base_url('setup/db-tools')); + } + + $migrationDir = APPPATH . 'Database/Migrations'; + if (!is_dir($migrationDir) && !mkdir($migrationDir, 0777, true) && !is_dir($migrationDir)) { + session()->setFlashdata('setup_msg', 'Unable to create migration directory.'); + return redirect()->to(base_url('setup/db-tools')); + } + + $createdFiles = []; + $ts = date('Y-m-d-His'); + $counter = 0; + + foreach ($selected as $tableName) { + $showQuery = $this->db->query('SHOW CREATE TABLE `' . $tableName . '`'); + $showRow = $showQuery->getRowArray(); + if (empty($showRow)) { + continue; + } + + $createSql = ''; + foreach ($showRow as $key => $value) { + if (is_string($key) && stripos($key, 'Create Table') !== false) { + $createSql = (string) $value; + break; + } + } + if ($createSql === '') { + $vals = array_values($showRow); + $createSql = (string) ($vals[1] ?? ''); + } + if ($createSql === '') { + continue; + } + + $createSql = preg_replace('/^CREATE\s+TABLE\s+/i', 'CREATE TABLE IF NOT EXISTS ', trim($createSql), 1) ?? $createSql; + if (!str_ends_with(trim($createSql), ';')) { + $createSql .= ';'; + } + + $className = 'CreateExistingTable' . $this->studly($tableName) . date('His') . str_pad((string) $counter, 2, '0', STR_PAD_LEFT); + $fileStamp = $ts . str_pad((string) $counter, 2, '0', STR_PAD_LEFT); + $fileName = $fileStamp . '_' . $className . '.php'; + $counter++; + + $php = "db->query(<<<'SQL'\n{$createSql}\nSQL\n );\n }\n\n public function down()\n {\n \$this->forge->dropTable('{$tableName}', true);\n }\n}\n"; + + $path = $migrationDir . DIRECTORY_SEPARATOR . $fileName; + if (@file_put_contents($path, $php) !== false) { + $createdFiles[] = $fileName; + } + } + + if (empty($createdFiles)) { + session()->setFlashdata('setup_msg', 'No migration files generated.'); + } else { + session()->setFlashdata('setup_msg', 'Generated ' . count($createdFiles) . ' migration file(s).'); + } + + return redirect()->to(base_url('setup/db-tools')); + } + + public function complete() + { + if ($deny = $this->ensureSetupAccess()) { + return $deny; + } + + $lockFile = $this->lockFilePath(); + $userName = 'unknown'; + if (function_exists('auth') && auth()->user()) { + $user = auth()->user(); + $userName = (string) ($user->username ?? $user->email ?? 'unknown'); + } + + $content = "Setup completed at: " . date('Y-m-d H:i:s') . "\nBy: " . $userName . "\n"; + @file_put_contents($lockFile, $content); + + return $this->response->setBody('Setup locked successfully. You can now remove setup routes/page if required.'); + } + + /** + * @return array + */ + private function detectScriptedTables(): array + { + $result = []; + $files = glob(APPPATH . 'Database/Migrations/*.php') ?: []; + + foreach ($files as $filePath) { + $content = @file_get_contents($filePath); + if (!is_string($content) || $content === '') { + continue; + } + + if (preg_match_all('/createTable\(\s*[\'\"]([a-zA-Z0-9_]+)[\'\"]/i', $content, $m1) === 1 || !empty($m1[1])) { + foreach (($m1[1] ?? []) as $table) { + $result[strtolower((string) $table)] = true; + } + } + + if (preg_match_all('/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?`?([a-zA-Z0-9_]+)`?/i', $content, $m2) === 1 || !empty($m2[1])) { + foreach (($m2[1] ?? []) as $table) { + $result[strtolower((string) $table)] = true; + } + } + } + + return $result; + } + + private function studly(string $value): string + { + $value = preg_replace('/[^a-zA-Z0-9]+/', ' ', $value) ?? $value; + $value = ucwords(strtolower(trim($value))); + return str_replace(' ', '', $value); + } +} diff --git a/app/Views/setup/db_setup.php b/app/Views/setup/db_setup.php new file mode 100644 index 0000000..5f162a4 --- /dev/null +++ b/app/Views/setup/db_setup.php @@ -0,0 +1,72 @@ +
+

One-Time DB Setup

+
+ +
+
+
+

Existing Database Tables

+
+
+ +
+ + +

+ Select unscripted tables to generate migration files. This does not modify existing data. + Generated files are placed in app/Database/Migrations. +

+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + +
PickTable NameMigration Script
No tables found.
+ + + + + + Scripted + + Missing + +
+
+ +
+ + Refresh +
+
+ +
+ +
+ + +

Lock file:

+
+
+
+