From 17af7fed1f2375a7dcd69813b7d6d34bd5c4a0d2 Mon Sep 17 00:00:00 2001 From: Crivion Date: Sat, 18 Oct 2025 18:06:58 +0300 Subject: [PATCH] mysql manager: refactoring to services & actions to keep my controller clean --- .../MySQL/GetCharsetsAndCollationsAction.php | 46 +++++ .../MySQL/GetDatabasesWithStatsAction.php | 68 +++++++ app/Http/Controllers/MysqlController.php | 187 +++--------------- app/Http/Requests/CreateDatabaseRequest.php | 63 ++++++ app/Http/Requests/DeleteDatabaseRequest.php | 28 +++ app/Http/Requests/UpdateDatabaseRequest.php | 31 +++ app/Policies/DatabasePolicy.php | 39 ++++ app/Providers/AuthServiceProvider.php | 30 +++ app/Services/MySQL/CreateDatabaseService.php | 65 ++++++ app/Services/MySQL/DeleteDatabaseService.php | 49 +++++ app/Services/MySQL/UpdateDatabaseService.php | 65 ++++++ bootstrap/providers.php | 1 + 12 files changed, 516 insertions(+), 156 deletions(-) create mode 100644 app/Actions/MySQL/GetCharsetsAndCollationsAction.php create mode 100644 app/Actions/MySQL/GetDatabasesWithStatsAction.php create mode 100644 app/Http/Requests/CreateDatabaseRequest.php create mode 100644 app/Http/Requests/DeleteDatabaseRequest.php create mode 100644 app/Http/Requests/UpdateDatabaseRequest.php create mode 100644 app/Policies/DatabasePolicy.php create mode 100644 app/Providers/AuthServiceProvider.php create mode 100644 app/Services/MySQL/CreateDatabaseService.php create mode 100644 app/Services/MySQL/DeleteDatabaseService.php create mode 100644 app/Services/MySQL/UpdateDatabaseService.php diff --git a/app/Actions/MySQL/GetCharsetsAndCollationsAction.php b/app/Actions/MySQL/GetCharsetsAndCollationsAction.php new file mode 100644 index 0000000..3f7fc3b --- /dev/null +++ b/app/Actions/MySQL/GetCharsetsAndCollationsAction.php @@ -0,0 +1,46 @@ + $this->getCharsets(), + 'collations' => $this->getCollations(), + ]; + } + + private function getCharsets(): array + { + $charsets = DB::select("SHOW CHARACTER SET"); + + return collect($charsets)->map(function($charset) { + return [ + 'name' => $charset->Charset, + 'description' => $charset->Description, + 'default_collation' => $charset->{'Default collation'}, + 'maxlen' => $charset->Maxlen, + ]; + })->toArray(); + } + + private function getCollations(): array + { + $collations = DB::select("SHOW COLLATION"); + + return collect($collations)->map(function($collation) { + return [ + 'name' => $collation->Collation, + 'charset' => $collation->Charset, + 'id' => $collation->Id, + 'default' => $collation->Default, + 'compiled' => $collation->Compiled, + 'sortlen' => $collation->Sortlen, + ]; + })->toArray(); + } +} diff --git a/app/Actions/MySQL/GetDatabasesWithStatsAction.php b/app/Actions/MySQL/GetDatabasesWithStatsAction.php new file mode 100644 index 0000000..971abc8 --- /dev/null +++ b/app/Actions/MySQL/GetDatabasesWithStatsAction.php @@ -0,0 +1,68 @@ +user->id)->get(); + $items = []; + + foreach ($databases as $database) { + $items[] = $this->buildDatabaseItem($database); + } + + return $items; + } + + private function buildDatabaseItem(Database $database): array + { + $dbName = $database->name; + + // Get table count + $tableCount = $this->getTableCount($dbName); + + // Get database size + $sizeMb = $this->getDatabaseSize($dbName); + + return [ + 'id' => $database->id, + 'name' => $database->name, + 'user' => $this->user->username, + 'db_user' => $database->db_user, + 'tables' => $tableCount, + 'sizeMb' => $sizeMb, + 'charset' => $database->charset, + 'collation' => $database->collation, + ]; + } + + private function getTableCount(string $dbName): int + { + $tables = DB::select( + "SELECT COUNT(*) as cnt FROM information_schema.tables WHERE table_schema = ?", + [$dbName] + ); + + return (int) ($tables[0]->cnt ?? 0); + } + + private function getDatabaseSize(string $dbName): float + { + $sizeRow = DB::selectOne( + "SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS size_mb + FROM information_schema.tables + WHERE table_schema = ?", + [$dbName] + ); + + return (float) ($sizeRow->size_mb ?? 0); + } +} diff --git a/app/Http/Controllers/MysqlController.php b/app/Http/Controllers/MysqlController.php index 0dcfe65..cce0e7d 100644 --- a/app/Http/Controllers/MysqlController.php +++ b/app/Http/Controllers/MysqlController.php @@ -2,206 +2,81 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; -use Inertia\Inertia; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Gate; -use Illuminate\Support\Facades\Process; +use App\Actions\MySQL\GetCharsetsAndCollationsAction; +use App\Actions\MySQL\GetDatabasesWithStatsAction; +use App\Http\Requests\CreateDatabaseRequest; +use App\Http\Requests\DeleteDatabaseRequest; +use App\Http\Requests\UpdateDatabaseRequest; use App\Models\Database; +use App\Services\MySQL\CreateDatabaseService; +use App\Services\MySQL\DeleteDatabaseService; +use App\Services\MySQL\UpdateDatabaseService; +use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; +use Illuminate\Http\JsonResponse; +use Inertia\Inertia; class MysqlController extends Controller { - public function index(Request $request): \Inertia\Response { $user = $request->user(); - - // Get databases from our model for the current user - $databases = Database::where('user_id', $user->id)->get(); - - $items = []; - foreach ($databases as $database) { - // Get additional info from MySQL - $dbName = $database->name; - - // number of tables - $tables = DB::select("SELECT COUNT(*) as cnt FROM information_schema.tables WHERE table_schema = ?", [$dbName]); - $tableCount = (int) ($tables[0]->cnt ?? 0); - - // total size (data + index) - $sizeRow = DB::selectOne("SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS size_mb FROM information_schema.tables WHERE table_schema = ?", [$dbName]); - $sizeMb = (float) ($sizeRow->size_mb ?? 0); - - $items[] = [ - 'id' => $database->id, - 'name' => $database->name, - 'user' => $user->username, - 'db_user' => $database->db_user, - 'tables' => $tableCount, - 'sizeMb' => $sizeMb, - 'charset' => $database->charset, - 'collation' => $database->collation, - ]; - } + $databases = (new GetDatabasesWithStatsAction($user))->execute(); return Inertia::render('Mysql/Index', [ - 'databases' => $items, + 'databases' => $databases, ]); } - public function getCharsetsAndCollations() + public function getCharsetsAndCollations(GetCharsetsAndCollationsAction $action): JsonResponse { - // Get available character sets - $charsets = DB::select("SHOW CHARACTER SET"); - $charsetData = collect($charsets)->map(function($charset) { - return [ - 'name' => $charset->Charset, - 'description' => $charset->Description, - 'default_collation' => $charset->{'Default collation'}, - 'maxlen' => $charset->Maxlen, - ]; - }); - - // Get available collations - $collations = DB::select("SHOW COLLATION"); - $collationData = collect($collations)->map(function($collation) { - return [ - 'name' => $collation->Collation, - 'charset' => $collation->Charset, - 'id' => $collation->Id, - 'default' => $collation->Default, - 'compiled' => $collation->Compiled, - 'sortlen' => $collation->Sortlen, - ]; - }); - - return response()->json([ - 'charsets' => $charsetData, - 'collations' => $collationData, - ]); + return response()->json($action->execute()); } - public function store(Request $request) + public function store(CreateDatabaseRequest $request): RedirectResponse { - $request->validate([ - 'name' => ['required', 'string'], - 'db_user' => ['required', 'string'], - 'db_pass' => ['required', 'string'], - 'charset' => ['required', 'string'], - 'collation' => ['required', 'string'], - ]); - $user = $request->user(); - $prefix = $user->username . '_'; + + (new CreateDatabaseService($request->validated(), $user))->handle(); - $name = $request->string('name'); - $dbUser = $request->string('db_user'); - $dbPass = $request->string('db_pass'); - $charset = $request->string('charset'); - $collation = $request->string('collation'); + session()->flash('success', 'Database created successfully!'); - if (!str_starts_with($name, $prefix)) { - return back()->withErrors(['name' => 'Database name must start with ' . $prefix]); - } - - if (!str_starts_with($dbUser, $prefix)) { - return back()->withErrors(['db_user' => 'Database username must start with ' . $prefix]); - } - - // Create database with charset and collation - DB::statement("CREATE DATABASE `$name` CHARACTER SET $charset COLLATE $collation"); - - // Create user (if not exists) and grant all privileges on the new DB - DB::statement("CREATE USER IF NOT EXISTS `$dbUser`@'localhost' IDENTIFIED BY '$dbPass'"); - DB::statement("GRANT ALL PRIVILEGES ON `$name`.* TO `$dbUser`@'localhost'"); - DB::statement("FLUSH PRIVILEGES"); - - // Create database record in our model - Database::create([ - 'name' => $name, - 'db_user' => $dbUser, - 'db_password' => $dbPass, - 'charset' => $charset, - 'collation' => $collation, - 'user_id' => $user->id, - ]); - - return redirect()->route('mysql.index')->with('success', 'Database created successfully.'); + return redirect()->route('mysql.index'); } - public function update(Request $request) + public function update(UpdateDatabaseRequest $request): RedirectResponse { - $request->validate([ - 'id' => ['required', 'integer'], - 'charset' => ['required', 'string'], - 'collation' => ['required', 'string'], - 'db_password' => ['nullable', 'string'], - ]); - $user = $request->user(); $databaseId = $request->integer('id'); - $charset = $request->string('charset'); - $collation = $request->string('collation'); - $newPassword = $request->string('db_password'); - // Find the database record $database = Database::where('id', $databaseId) ->where('user_id', $user->id) ->firstOrFail(); - $name = $database->name; + $this->authorize('update', $database); - // Update database charset and collation in MySQL - DB::statement("ALTER DATABASE `$name` CHARACTER SET $charset COLLATE $collation"); + (new UpdateDatabaseService($database, $request->validated()))->handle(); - // Update the database record - $updateData = [ - 'charset' => $charset, - 'collation' => $collation, - ]; + session()->flash('success', 'Database updated successfully!'); - // Update password if provided - if ($request->filled('db_password')) { - $updateData['db_password'] = $newPassword; - - // Update MySQL user password - DB::statement("ALTER USER `{$database->db_user}`@'localhost' IDENTIFIED BY '$newPassword'"); - DB::statement("FLUSH PRIVILEGES"); - } - - $database->update($updateData); - - return redirect()->route('mysql.index')->with('success', 'Database charset and collation updated successfully.'); + return redirect()->route('mysql.index'); } - - public function destroy(Request $request) + public function destroy(DeleteDatabaseRequest $request): RedirectResponse { - $request->validate([ - 'id' => ['required', 'integer'], - ]); - $user = $request->user(); $databaseId = $request->integer('id'); - // Find the database record $database = Database::where('id', $databaseId) ->where('user_id', $user->id) ->firstOrFail(); - $name = $database->name; - $dbUser = $database->db_user; + $this->authorize('delete', $database); - // Drop the MySQL database - DB::statement("DROP DATABASE IF EXISTS `$name`"); + (new DeleteDatabaseService($database))->handle(); - // Drop the MySQL user - DB::statement("DROP USER IF EXISTS `$dbUser`@'localhost'"); - DB::statement("FLUSH PRIVILEGES"); + session()->flash('success', 'Database deleted successfully!'); - // Delete the database record - $database->delete(); - - return redirect()->route('mysql.index')->with('success', 'Database deleted successfully.'); + return redirect()->route('mysql.index'); } } diff --git a/app/Http/Requests/CreateDatabaseRequest.php b/app/Http/Requests/CreateDatabaseRequest.php new file mode 100644 index 0000000..6d46c79 --- /dev/null +++ b/app/Http/Requests/CreateDatabaseRequest.php @@ -0,0 +1,63 @@ +|string> + */ + public function rules(): array + { + $user = $this->user(); + $prefix = $user->username . '_'; + + return [ + 'name' => [ + 'required', + 'string', + 'max:64', + 'regex:/^' . preg_quote($prefix) . '[a-zA-Z0-9_]+$/', + 'unique:' . Database::class . ',name' + ], + 'db_user' => [ + 'required', + 'string', + 'max:32', + 'regex:/^' . preg_quote($prefix) . '[a-zA-Z0-9_]+$/' + ], + 'db_pass' => ['required', 'string', 'min:8'], + 'charset' => ['required', 'string'], + 'collation' => ['required', 'string'], + ]; + } + + /** + * Get custom messages for validator errors. + */ + public function messages(): array + { + $user = $this->user(); + $prefix = $user->username . '_'; + + return [ + 'name.regex' => 'Database name must start with ' . $prefix . ' and contain only letters, numbers, and underscores.', + 'db_user.regex' => 'Database username must start with ' . $prefix . ' and contain only letters, numbers, and underscores.', + 'name.unique' => 'A database with this name already exists.', + ]; + } +} diff --git a/app/Http/Requests/DeleteDatabaseRequest.php b/app/Http/Requests/DeleteDatabaseRequest.php new file mode 100644 index 0000000..2779b84 --- /dev/null +++ b/app/Http/Requests/DeleteDatabaseRequest.php @@ -0,0 +1,28 @@ +|string> + */ + public function rules(): array + { + return [ + 'id' => ['required', 'integer'], + ]; + } +} diff --git a/app/Http/Requests/UpdateDatabaseRequest.php b/app/Http/Requests/UpdateDatabaseRequest.php new file mode 100644 index 0000000..c212c6d --- /dev/null +++ b/app/Http/Requests/UpdateDatabaseRequest.php @@ -0,0 +1,31 @@ +|string> + */ + public function rules(): array + { + return [ + 'id' => ['required', 'integer'], + 'charset' => ['required', 'string'], + 'collation' => ['required', 'string'], + 'db_password' => ['nullable', 'string', 'min:8'], + ]; + } +} diff --git a/app/Policies/DatabasePolicy.php b/app/Policies/DatabasePolicy.php new file mode 100644 index 0000000..063c2c6 --- /dev/null +++ b/app/Policies/DatabasePolicy.php @@ -0,0 +1,39 @@ +isAdmin() || $user->id === $database->user_id) + ? Response::allow() + : Response::deny('You are not authorized to view this database.'); + } + + public function update(User $user, Database $database): Response + { + return ($user->isAdmin() || $user->id === $database->user_id) + ? Response::allow() + : Response::deny('You are not authorized to update this database.'); + } + + public function delete(User $user, Database $database): Response + { + return ($user->isAdmin() || $user->id === $database->user_id) + ? Response::allow() + : Response::deny('You are not authorized to delete this database.'); + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php new file mode 100644 index 0000000..75cdf0a --- /dev/null +++ b/app/Providers/AuthServiceProvider.php @@ -0,0 +1,30 @@ + + */ + protected $policies = [ + Website::class => WebsitePolicy::class, + Database::class => DatabasePolicy::class, + ]; + + /** + * Register any authentication / authorization services. + */ + public function boot(): void + { + // + } +} diff --git a/app/Services/MySQL/CreateDatabaseService.php b/app/Services/MySQL/CreateDatabaseService.php new file mode 100644 index 0000000..9f9a33c --- /dev/null +++ b/app/Services/MySQL/CreateDatabaseService.php @@ -0,0 +1,65 @@ +createMySQLDatabase(); + $this->createMySQLUser(); + + return $this->createDatabaseRecord(); + } + + private function createMySQLDatabase(): void + { + $name = $this->validated['name']; + $charset = $this->validated['charset']; + $collation = $this->validated['collation']; + + try { + DB::statement("CREATE DATABASE `$name` CHARACTER SET $charset COLLATE $collation"); + } catch (Exception $e) { + throw new CreateDatabaseException('Failed to create MySQL database: ' . $e->getMessage()); + } + } + + private function createMySQLUser(): void + { + $dbUser = $this->validated['db_user']; + $dbPass = $this->validated['db_pass']; + $name = $this->validated['name']; + + try { + DB::statement("CREATE USER IF NOT EXISTS `$dbUser`@'localhost' IDENTIFIED BY '$dbPass'"); + DB::statement("GRANT ALL PRIVILEGES ON `$name`.* TO `$dbUser`@'localhost'"); + DB::statement("FLUSH PRIVILEGES"); + } catch (Exception $e) { + // Rollback database creation if user creation fails + DB::statement("DROP DATABASE IF EXISTS `{$this->validated['name']}`"); + throw new CreateDatabaseException('Failed to create MySQL user: ' . $e->getMessage()); + } + } + + private function createDatabaseRecord(): Database + { + return Database::create([ + 'name' => $this->validated['name'], + 'db_user' => $this->validated['db_user'], + 'db_password' => $this->validated['db_pass'], + 'charset' => $this->validated['charset'], + 'collation' => $this->validated['collation'], + 'user_id' => $this->user->id, + ]); + } +} diff --git a/app/Services/MySQL/DeleteDatabaseService.php b/app/Services/MySQL/DeleteDatabaseService.php new file mode 100644 index 0000000..2c0bdd1 --- /dev/null +++ b/app/Services/MySQL/DeleteDatabaseService.php @@ -0,0 +1,49 @@ +dropMySQLDatabase(); + $this->dropMySQLUser(); + $this->deleteDatabaseRecord(); + } + + private function dropMySQLDatabase(): void + { + $name = $this->database->name; + + try { + DB::statement("DROP DATABASE IF EXISTS `$name`"); + } catch (Exception $e) { + throw new DeleteDatabaseException('Failed to drop MySQL database: ' . $e->getMessage()); + } + } + + private function dropMySQLUser(): void + { + $dbUser = $this->database->db_user; + + try { + DB::statement("DROP USER IF EXISTS `$dbUser`@'localhost'"); + DB::statement("FLUSH PRIVILEGES"); + } catch (Exception $e) { + throw new DeleteDatabaseException('Failed to drop MySQL user: ' . $e->getMessage()); + } + } + + private function deleteDatabaseRecord(): void + { + $this->database->delete(); + } +} diff --git a/app/Services/MySQL/UpdateDatabaseService.php b/app/Services/MySQL/UpdateDatabaseService.php new file mode 100644 index 0000000..d607093 --- /dev/null +++ b/app/Services/MySQL/UpdateDatabaseService.php @@ -0,0 +1,65 @@ +updateMySQLDatabase(); + $this->updateMySQLUserPassword(); + $this->updateDatabaseRecord(); + } + + private function updateMySQLDatabase(): void + { + $name = $this->database->name; + $charset = $this->validated['charset']; + $collation = $this->validated['collation']; + + try { + DB::statement("ALTER DATABASE `$name` CHARACTER SET $charset COLLATE $collation"); + } catch (Exception $e) { + throw new UpdateDatabaseException('Failed to update MySQL database charset/collation: ' . $e->getMessage()); + } + } + + private function updateMySQLUserPassword(): void + { + if (!isset($this->validated['db_password']) || empty($this->validated['db_password'])) { + return; + } + + $dbUser = $this->database->db_user; + $newPassword = $this->validated['db_password']; + + try { + DB::statement("ALTER USER `$dbUser`@'localhost' IDENTIFIED BY '$newPassword'"); + DB::statement("FLUSH PRIVILEGES"); + } catch (Exception $e) { + throw new UpdateDatabaseException('Failed to update MySQL user password: ' . $e->getMessage()); + } + } + + private function updateDatabaseRecord(): void + { + $updateData = [ + 'charset' => $this->validated['charset'], + 'collation' => $this->validated['collation'], + ]; + + if (isset($this->validated['db_password']) && !empty($this->validated['db_password'])) { + $updateData['db_password'] = $this->validated['db_password']; + } + + $this->database->update($updateData); + } +} diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 3e33af0..9948d50 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -2,5 +2,6 @@ return [ App\Providers\AppServiceProvider::class, + App\Providers\AuthServiceProvider::class, Lab404\Impersonate\ImpersonateServiceProvider::class, ];