From 7f4e68bc617fb665bd99d2582fee89cb997cc62e Mon Sep 17 00:00:00 2001 From: Crivion Date: Sat, 18 Oct 2025 17:28:12 +0300 Subject: [PATCH] mysql: update db user password too - keep record of dbs created in a table --- app/Http/Controllers/MysqlController.php | 132 +++++++++--------- app/Models/Database.php | 47 +++++++ ...25_10_18_142123_create_databases_table.php | 35 +++++ resources/js/Pages/Mysql/Index.jsx | 6 +- .../Pages/Mysql/Partials/EditDatabaseForm.jsx | 37 +++-- routes/web.php | 1 - 6 files changed, 173 insertions(+), 85 deletions(-) create mode 100644 app/Models/Database.php create mode 100644 database/migrations/2025_10_18_142123_create_databases_table.php diff --git a/app/Http/Controllers/MysqlController.php b/app/Http/Controllers/MysqlController.php index 7bb2ad6..bb38d4a 100644 --- a/app/Http/Controllers/MysqlController.php +++ b/app/Http/Controllers/MysqlController.php @@ -7,6 +7,7 @@ use Inertia\Inertia; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Process; +use App\Models\Database; class MysqlController extends Controller { @@ -14,18 +15,15 @@ class MysqlController extends Controller public function index(Request $request): \Inertia\Response { $user = $request->user(); - $prefix = $user->username . '_'; - // collect databases for current user - $databases = DB::select("SHOW DATABASES"); - $dbNames = collect($databases) - ->map(fn($row) => (array) $row) - ->map(fn($row) => reset($row)) - ->filter(fn($name) => str_starts_with($name, $prefix)) - ->values(); + // Get databases from our model for the current user + $databases = Database::where('user_id', $user->id)->get(); $items = []; - foreach ($dbNames as $dbName) { + 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); @@ -34,16 +32,15 @@ class MysqlController extends Controller $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); - // default engine and charset from schema - $schema = DB::selectOne("SELECT DEFAULT_CHARACTER_SET_NAME as charset, DEFAULT_COLLATION_NAME as collation FROM information_schema.schemata WHERE schema_name = ?", [$dbName]); - $items[] = [ - 'name' => $dbName, + 'id' => $database->id, + 'name' => $database->name, 'user' => $user->username, + 'db_user' => $database->db_user, 'tables' => $tableCount, 'sizeMb' => $sizeMb, - 'charset' => $schema->charset ?? null, - 'collation' => $schema->collation ?? null, + 'charset' => $database->charset, + 'collation' => $database->collation, ]; } @@ -119,91 +116,92 @@ class MysqlController extends Controller 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.'); } public function update(Request $request) { $request->validate([ - 'name' => ['required', 'string'], + 'id' => ['required', 'integer'], 'charset' => ['required', 'string'], 'collation' => ['required', 'string'], + 'db_password' => ['nullable', 'string'], ]); $user = $request->user(); - $prefix = $user->username . '_'; - - $name = $request->string('name'); + $databaseId = $request->integer('id'); $charset = $request->string('charset'); $collation = $request->string('collation'); + $newPassword = $request->string('db_password'); - if (!str_starts_with($name, $prefix)) { - return back()->withErrors(['name' => 'Database name must start with ' . $prefix]); - } + // Find the database record + $database = Database::where('id', $databaseId) + ->where('user_id', $user->id) + ->firstOrFail(); - // Check if database exists - $databases = DB::select("SHOW DATABASES"); - $dbNames = collect($databases) - ->map(fn($row) => (array) $row) - ->map(fn($row) => reset($row)) - ->filter(fn($dbName) => str_starts_with($dbName, $prefix)) - ->values(); + $name = $database->name; - if (!$dbNames->contains($name)) { - return back()->withErrors(['name' => 'Database not found or access denied']); - } - - // Update database charset and collation + // Update database charset and collation in MySQL DB::statement("ALTER DATABASE `$name` CHARACTER SET $charset COLLATE $collation"); - return redirect()->route('mysql.index')->with('success', 'Database updated successfully.'); - } + // Update the database record + $updateData = [ + 'charset' => $charset, + 'collation' => $collation, + ]; - public function rename(Request $request) - { - $request->validate([ - 'from' => ['required', 'string'], - 'to' => ['required', 'string'], - ]); - - $user = $request->user(); - $prefix = $user->username . '_'; - - $from = $request->string('from'); - $to = $request->string('to'); - - if (!str_starts_with($from, $prefix) || !str_starts_with($to, $prefix)) { - return back()->withErrors(['to' => 'Database names must start with ' . $prefix]); + // Update password if provided + if ($newPassword) { + $updateData['db_password'] = $newPassword; + + // Update MySQL user password + DB::statement("ALTER USER `{$database->db_user}`@'localhost' IDENTIFIED BY '$newPassword'"); + DB::statement("FLUSH PRIVILEGES"); } - // MySQL has no native RENAME DATABASE; approach: create new DB, move tables, drop old - DB::statement("CREATE DATABASE IF NOT EXISTS `$to`"); - $tables = DB::select("SELECT table_name FROM information_schema.tables WHERE table_schema = ?", [$from]); - foreach ($tables as $t) { - $table = $t->table_name ?? reset((array)$t); - DB::statement("RENAME TABLE `$from`.`$table` TO `$to`.`$table`"); - } - DB::statement("DROP DATABASE `$from`"); + $database->update($updateData); - return back()->with('success', 'Database renamed successfully.'); + return redirect()->route('mysql.index')->with('success', 'Database charset and collation updated successfully.'); } + public function destroy(Request $request) { $request->validate([ - 'name' => ['required', 'string'], + 'id' => ['required', 'integer'], ]); $user = $request->user(); - $prefix = $user->username . '_'; - $name = $request->string('name'); + $databaseId = $request->integer('id'); - if (!str_starts_with($name, $prefix)) { - return back()->withErrors(['name' => 'Database name must start with ' . $prefix]); - } + // Find the database record + $database = Database::where('id', $databaseId) + ->where('user_id', $user->id) + ->firstOrFail(); + $name = $database->name; + $dbUser = $database->db_user; + + // Drop the MySQL database DB::statement("DROP DATABASE IF EXISTS `$name`"); - return back()->with('success', 'Database deleted successfully.'); + // Drop the MySQL user + DB::statement("DROP USER IF EXISTS `$dbUser`@'localhost'"); + DB::statement("FLUSH PRIVILEGES"); + + // Delete the database record + $database->delete(); + + return redirect()->route('mysql.index')->with('success', 'Database deleted successfully.'); } } diff --git a/app/Models/Database.php b/app/Models/Database.php new file mode 100644 index 0000000..6462665 --- /dev/null +++ b/app/Models/Database.php @@ -0,0 +1,47 @@ + 'encrypted', + ]; + + /** + * Get the user that owns the database. + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + /** + * Get the decrypted database password. + */ + public function getDecryptedPasswordAttribute(): string + { + return decrypt($this->db_password); + } + + /** + * Set the encrypted database password. + */ + public function setPasswordAttribute(string $password): void + { + $this->attributes['db_password'] = encrypt($password); + } +} diff --git a/database/migrations/2025_10_18_142123_create_databases_table.php b/database/migrations/2025_10_18_142123_create_databases_table.php new file mode 100644 index 0000000..9dc02de --- /dev/null +++ b/database/migrations/2025_10_18_142123_create_databases_table.php @@ -0,0 +1,35 @@ +id(); + $table->string('name')->unique(); + $table->string('db_user'); + $table->text('db_password'); // Encrypted password + $table->string('charset')->default('utf8mb4'); + $table->string('collation')->default('utf8mb4_unicode_ci'); + $table->foreignId('user_id')->constrained()->onDelete('cascade'); + $table->timestamps(); + + $table->index(['user_id', 'name']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('databases'); + } +}; diff --git a/resources/js/Pages/Mysql/Index.jsx b/resources/js/Pages/Mysql/Index.jsx index ae41569..02d3c4f 100644 --- a/resources/js/Pages/Mysql/Index.jsx +++ b/resources/js/Pages/Mysql/Index.jsx @@ -12,9 +12,9 @@ export default function MysqlIndex({ databases = [] }) { const { auth } = usePage().props; - const deleteDb = (name) => { + const deleteDb = (id) => { router.delete(route('mysql.destroy'), { - data: { name }, + data: { id }, onBefore: () => toast('Deleting database...'), onSuccess: () => toast('Database deleted.'), onError: () => toast('Failed to delete database.'), @@ -61,7 +61,7 @@ export default function MysqlIndex({ databases = [] }) {
- deleteDb(db.name)}> + deleteDb(db.id)}>
diff --git a/resources/js/Pages/Mysql/Partials/EditDatabaseForm.jsx b/resources/js/Pages/Mysql/Partials/EditDatabaseForm.jsx index 16adc43..f8afde6 100644 --- a/resources/js/Pages/Mysql/Partials/EditDatabaseForm.jsx +++ b/resources/js/Pages/Mysql/Partials/EditDatabaseForm.jsx @@ -20,9 +20,10 @@ export default function EditDatabaseForm({ database }) { const [loading, setLoading] = useState(false); const { data, setData, patch, processing, reset, clearErrors, errors } = useForm({ - name: database.name, + id: database.id, charset: database.charset || 'utf8mb4', collation: database.collation || 'utf8mb4_unicode_ci', + db_password: '', }); useEffect(() => { @@ -98,15 +99,13 @@ export default function EditDatabaseForm({ database }) { }); }; - const prefix = auth.user.username + '_'; - return ( <>