mirror of
https://github.com/crivion/laranode.git
synced 2026-06-01 03:22:44 +08:00
mysql: update db user password too - keep record of dbs created in a table
This commit is contained in:
@@ -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.');
|
||||
}
|
||||
}
|
||||
|
||||
47
app/Models/Database.php
Normal file
47
app/Models/Database.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
|
||||
class Database extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'db_user',
|
||||
'db_password',
|
||||
'charset',
|
||||
'collation',
|
||||
'user_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'db_password' => '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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('databases', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -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 = [] }) {
|
||||
<td className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||
<div className='flex items-center space-x-2'>
|
||||
<EditDatabaseForm database={db} />
|
||||
<ConfirmationButton doAction={() => deleteDb(db.name)}>
|
||||
<ConfirmationButton doAction={() => deleteDb(db.id)}>
|
||||
<TiDelete className='w-6 h-6 text-red-500' />
|
||||
</ConfirmationButton>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<button
|
||||
onClick={showEditModal}
|
||||
className="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
data-tooltip-id={`tooltip-edit-${database.name}`}
|
||||
data-tooltip-content="Edit Database"
|
||||
data-tooltip-content="Edit Charset & Collation"
|
||||
data-tooltip-place="top"
|
||||
>
|
||||
<FaEdit className='w-4 h-4' />
|
||||
@@ -121,17 +120,27 @@ export default function EditDatabaseForm({ database }) {
|
||||
|
||||
<div className="mt-6 flex flex-col space-y-4 max-h-[500px]">
|
||||
<div>
|
||||
<InputLabel htmlFor="name" value={`Database name (must start with ${prefix})`} className='my-2' />
|
||||
<InputLabel htmlFor="db_user" value="Database User" className='my-2' />
|
||||
<TextInput
|
||||
id="name"
|
||||
name="name"
|
||||
value={data.name}
|
||||
onChange={(e) => setData('name', e.target.value)}
|
||||
className="mt-1 block w-full"
|
||||
placeholder={prefix + 'mydb'}
|
||||
required
|
||||
id="db_user"
|
||||
name="db_user"
|
||||
value={database.db_user}
|
||||
className="mt-1 block w-full bg-gray-100 dark:bg-gray-800"
|
||||
disabled
|
||||
/>
|
||||
<InputError message={errors.name} className="mt-2" />
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel htmlFor="db_password" value="Database Password (leave blank to keep current)" className='my-2' />
|
||||
<TextInput
|
||||
id="db_password"
|
||||
name="db_password"
|
||||
type="password"
|
||||
value={data.db_password}
|
||||
onChange={(e) => setData('db_password', e.target.value)}
|
||||
className="mt-1 block w-full"
|
||||
placeholder="Enter new password or leave blank"
|
||||
/>
|
||||
<InputError message={errors.db_password} className="mt-2" />
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel htmlFor="charset" value="Charset" className='my-2' />
|
||||
@@ -164,7 +173,7 @@ export default function EditDatabaseForm({ database }) {
|
||||
<InputError message={errors.collation} className="mt-2" />
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<PrimaryButton className="mr-3" disabled={processing}>Update Database</PrimaryButton>
|
||||
<PrimaryButton className="mr-3" disabled={processing}>Update Charset & Collation</PrimaryButton>
|
||||
<SecondaryButton onClick={closeModal}>Cancel</SecondaryButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -39,7 +39,6 @@ Route::get('/mysql', [MysqlController::class, 'index'])->middleware(['auth'])->n
|
||||
Route::get('/mysql/charsets-collations', [MysqlController::class, 'getCharsetsAndCollations'])->middleware(['auth'])->name('mysql.charsets-collations');
|
||||
Route::post('/mysql', [MysqlController::class, 'store'])->middleware(['auth'])->name('mysql.store');
|
||||
Route::patch('/mysql', [MysqlController::class, 'update'])->middleware(['auth'])->name('mysql.update');
|
||||
Route::patch('/mysql/rename', [MysqlController::class, 'rename'])->middleware(['auth'])->name('mysql.rename');
|
||||
Route::delete('/mysql', [MysqlController::class, 'destroy'])->middleware(['auth'])->name('mysql.destroy');
|
||||
|
||||
// Filemanager [Admin | User]
|
||||
|
||||
Reference in New Issue
Block a user