Backend: real endpoints for role-permission overview, MQTT broker settings (with live reachability check), and SQLite backup download/restore (new system.manage permission, super-admin only; restore validates the SQLite file header and keeps a pre-restore safety copy). Desktop frontend: Roles (read-only permissions view), MQTT Settings, Backup & Restore wired to those endpoints; plus static AdminLTE-matching shells for Profile, Settings (embeds the real 2FA flow in its Security tab), Invoice, Calendar (FullCalendar), Chat, File Manager, and Projects. Mobile frontend: new frontend/src/styles/mobile.css, a from-scratch design system (purple theme, cards, slide-out sidebar, bottom tab bar) scoped entirely under a .mob-app root and mob- prefixed classes so it can never collide with the desktop Bootstrap/AdminLTE styling. Rebuilt TopBar/ BottomNav, added a new SlideOutNav, and reskinned the Dashboard and Users pages to it with real data.
59 lines
1.8 KiB
PHP
59 lines
1.8 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Services\Audit\AuditLogger;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class BackupController extends Controller
|
|
{
|
|
private const SQLITE_HEADER = "SQLite format 3\000";
|
|
|
|
public function download(Request $request, AuditLogger $audit)
|
|
{
|
|
abort_unless($request->user()->can('system.manage'), 403);
|
|
|
|
$path = config('database.connections.sqlite.database');
|
|
abort_unless($path && is_file($path), 404, 'No database file to back up.');
|
|
|
|
$audit->log($request->user(), 'system.backup_downloaded');
|
|
|
|
return response()->download($path, 'backup-'.now()->format('Y-m-d-His').'.sqlite');
|
|
}
|
|
|
|
public function restore(Request $request, AuditLogger $audit)
|
|
{
|
|
abort_unless($request->user()->can('system.manage'), 403);
|
|
|
|
$request->validate(['backup' => ['required', 'file']]);
|
|
|
|
$uploaded = $request->file('backup');
|
|
$header = file_get_contents($uploaded->getRealPath(), false, null, 0, 16);
|
|
|
|
if ($header !== self::SQLITE_HEADER) {
|
|
throw ValidationException::withMessages([
|
|
'backup' => 'That file is not a valid SQLite database.',
|
|
]);
|
|
}
|
|
|
|
$path = config('database.connections.sqlite.database');
|
|
|
|
// Release Laravel's open handle on the current file before swapping it.
|
|
DB::disconnect();
|
|
|
|
$safetyCopy = dirname($path).'/pre-restore-'.now()->format('Y-m-d-His').'.sqlite';
|
|
if (is_file($path)) {
|
|
copy($path, $safetyCopy);
|
|
}
|
|
|
|
$uploaded->move(dirname($path), basename($path));
|
|
|
|
$audit->log($request->user(), 'system.backup_restored');
|
|
|
|
return response()->noContent();
|
|
}
|
|
}
|