Files
BarangaySystem/app/Http/Controllers/Market/UltimateController.php
2026-06-06 18:43:00 +08:00

566 lines
22 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Http\Controllers\Market;
use App\Enums\UserActions;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\User;
use App\Models\Market\Store;
use App\Models\Market\Product;
use App\Models\GlobalTransaction;
use App\Models\FileContent;
use App\Models\DbBackup;
use Hyperf\Stringable\Str;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\DB;
use Hypervel\Support\Facades\Redis;
use Hypervel\Support\Facades\Response;
class UltimateController
{
/**
* Common check for Ultimate access.
*/
private function checkAccess()
{
if (!UserPermissions::isActionPermitted(0, UserActions::UltimateConsole)) {
return false;
}
return true;
}
/**
* Get system-wide statistics for the dashboard.
*/
public function getSystemStats()
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$globalMessage = Redis::get('system:global_message');
$redisStatus = ['connected' => false, 'ping_ms' => null, 'used_memory_human' => null, 'version' => null, 'error' => null];
try {
$start = microtime(true);
$pong = Redis::ping();
$redisStatus['ping_ms'] = round((microtime(true) - $start) * 1000, 2);
$redisStatus['connected'] = $pong === true || $pong === 'PONG' || $pong === '+PONG' || (is_string($pong) && stripos($pong, 'PONG') !== false);
$info = Redis::info();
if (is_array($info)) {
$flat = isset($info['Memory']) && is_array($info['Memory']) ? $info['Memory'] : $info;
$redisStatus['used_memory_human'] = $flat['used_memory_human'] ?? null;
$serverInfo = isset($info['Server']) && is_array($info['Server']) ? $info['Server'] : $info;
$redisStatus['version'] = $serverInfo['redis_version'] ?? null;
}
} catch (\Throwable $e) {
$redisStatus['error'] = $e->getMessage();
}
$stats = [
'users' => User::count(),
'active_users' => User::where('active', true)->count(),
'stores' => Store::count(),
'active_stores' => Store::where('is_active', true)->count(),
'products' => Product::count(),
'transactions' => GlobalTransaction::count(),
'total_balance' => GlobalTransaction::sum('amount'),
'php_version' => PHP_VERSION,
'server_time' => date('Y-m-d H:i:s'),
'maintenance_mode' => Redis::get('system:maintenance_mode') === 'true',
'global_message' => $globalMessage ? json_decode($globalMessage, true) : null,
'logs_count' => DB::table('logs')->count(),
'table_logs_count' => DB::table('table_logs')->count(),
'pos_sessions_count' => DB::table('pos_sessions')->count(),
'cooperatives_count' => DB::table('organizations')->where('type', 'COOPERATIVE')->count(),
'carts_count' => DB::table('carts')->count(),
'farmer_profiles_count' => DB::table('farmer_profiles')->count(),
'redis' => $redisStatus,
];
return Response::json(['success' => true, 'data' => $stats]);
}
/**
* Execute a raw SQL query.
*/
public function runQuery(Request $request)
{
if (Auth::user()->acct_type !== \App\Enums\UserTypes::ULTIMATE || !UserPermissions::isActionPermitted(0, UserActions::UltimateQuery)) {
return ResponseHelper::returnUnauthorized();
}
$query = $request->input('query');
if (empty($query)) return ResponseHelper::returnError('Query cannot be empty');
try {
$queryLower = strtolower(trim($query));
if (str_starts_with($queryLower, 'select') || str_starts_with($queryLower, 'show') || str_starts_with($queryLower, 'describe')) {
$results = DB::select($query);
return Response::json(['success' => true, 'data' => $results]);
} else {
$affected = DB::statement($query);
return Response::json(['success' => true, 'affected' => $affected]);
}
} catch (\Throwable $th) {
return ResponseHelper::returnError($th->getMessage());
}
}
/**
* Toggle maintenance mode system-wide.
*/
public function toggleMaintenance(Request $request)
{
if (!$this->checkAccess() || !UserPermissions::isActionPermitted(0, UserActions::UltimateMaintenance)) {
return ResponseHelper::returnUnauthorized();
}
$enabled = (bool) $request->input('enabled');
Redis::set('system:maintenance_mode', $enabled ? 'true' : 'false');
return Response::json(['success' => true, 'maintenance_mode' => $enabled]);
}
/**
* Send a global message / broadcast.
*/
public function sendGlobalMessage(Request $request)
{
if (!$this->checkAccess() || !UserPermissions::isActionPermitted(0, UserActions::UltimateGlobalMessage)) {
return ResponseHelper::returnUnauthorized();
}
$message = $request->input('message');
$type = $request->input('type', 'info'); // info, success, warning, danger
if (empty($message)) {
Redis::del('system:global_message');
return Response::json(['success' => true, 'message' => 'Global message cleared']);
}
Redis::set('system:global_message', json_encode([
'text' => $message,
'type' => $type,
'timestamp' => time()
]));
return Response::json(['success' => true]);
}
/**
* Flush / Truncate specific tables.
*/
public function flushData(Request $request)
{
if (!$this->checkAccess() || !UserPermissions::isActionPermitted(0, UserActions::UltimateFlush)) {
return ResponseHelper::returnUnauthorized();
}
$target = $request->input('target');
try {
$affected = 0;
switch ($target) {
case 'transactions':
$affected = GlobalTransaction::count();
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
GlobalTransaction::truncate();
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
break;
case 'pos_sessions':
$affected = DB::table('pos_sessions')->count();
DB::table('pos_sessions')->truncate();
break;
case 'cache':
Redis::flushDB();
break;
case 'stores':
$affected = DB::table('str')->count();
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
DB::table('prd_str')->truncate();
DB::table('store_managers')->truncate();
DB::table('str')->truncate();
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
break;
case 'products':
$affected = DB::table('prd_items')->count();
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
DB::table('prd_str')->truncate();
DB::table('prd_items')->truncate();
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
break;
case 'cooperatives':
$affected = DB::table('organizations')->where('type', 'COOPERATIVE')->count();
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
DB::table('cooperative_votes')->truncate();
DB::table('cooperative_resolutions')->truncate();
DB::table('cooperative_documents')->truncate();
DB::table('cooperative_members')->truncate();
DB::table('organizations')->where('type', 'COOPERATIVE')->delete();
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
break;
case 'carts':
$affected = DB::table('carts')->count();
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
DB::table('cart_items')->truncate();
DB::table('carts')->truncate();
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
break;
case 'farmer_profiles':
$affected = DB::table('farmer_profiles')->count();
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
DB::table('farmer_profiles')->truncate();
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
break;
default:
return ResponseHelper::returnError('Invalid flush target');
}
return Response::json(['success' => true, 'message' => "Flushed $target successfully", 'affected' => $affected]);
} catch (\Throwable $th) {
return ResponseHelper::returnError($th->getMessage());
}
}
/**
* Trigger a test notification for a specific user.
*/
public function testNotification(Request $request)
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$userHash = $request->input('user_hash');
$user = User::where('hashkey', $userHash)->first();
if (!$user) return ResponseHelper::returnError('User not found');
// Setting exec_command which SSEController picks up to notify client
$user->exec_command = 'toast:success:Test Notification from Ultimate Console: ' . date('H:i:s');
$user->save();
return Response::json(['success' => true]);
}
/**
* Batch management for various entities.
*/
public function batchManage(Request $request)
{
if (!$this->checkAccess() || !UserPermissions::isActionPermitted(0, UserActions::UltimateBatch)) {
return ResponseHelper::returnUnauthorized();
}
$action = $request->input('action');
$ids = $request->input('ids', []);
$data = $request->input('data', []);
if (empty($ids) && !in_array($action, ['cleanup_sessions'])) {
return ResponseHelper::returnError('No IDs provided');
}
try {
switch($action) {
case 'activate_users':
User::whereIn('id', $ids)->update(['active' => true]);
break;
case 'deactivate_users':
User::whereIn('id', $ids)->update(['active' => false]);
break;
case 'cleanup_sessions':
DB::table('pos_sessions')->where('status', 'VOIDED')->delete();
break;
case 'mass_transfer_points':
$amount = (float)($data['amount'] ?? 0);
if ($amount <= 0) return ResponseHelper::returnError('Invalid amount');
foreach ($ids as $id) {
GlobalTransaction::create([
'user_id' => $id,
'amount' => $amount,
'type' => 'REWARD',
'description' => 'Mass points adjustment via Ultimate Console',
'is_active' => true,
]);
}
break;
default:
return ResponseHelper::returnError('Invalid batch action');
}
return Response::json(['success' => true]);
} catch (\Throwable $th) {
return ResponseHelper::returnError($th->getMessage());
}
}
/**
* Run a system command (Artisan wrapper).
*/
public function runCommand(Request $request)
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$command = $request->input('command');
if (empty($command)) return ResponseHelper::returnError('Command cannot be empty');
// Normalize command: strip 'php artisan ' if present
$command = preg_replace('/^php artisan\s+/', '', trim($command));
// Mapping for user-friendly commands
if ($command === 'reset-app all users') {
$command = 'app:reset-users';
}
if ($command === 'db seed') {
$command = 'db:seed';
}
// For security, only allow specific commands
$allowedCommands = [
'cache:clear', 'view:clear', 'config:clear', 'route:clear',
'migrate', 'migrate:rollback', 'migrate:fresh',
'db:seed', 'app:reset-users', 'optimize', 'optimize:clear'
];
$baseCommand = explode(' ', trim($command))[0];
if (!in_array($baseCommand, $allowedCommands)) {
return ResponseHelper::returnError("Command '{$baseCommand}' not allowed for security reasons.");
}
try {
// In Hyperf, running commands from HTTP request context is tricky.
// We'll use shell_exec in this local environment demo as a fallback.
$output = shell_exec("php artisan $command 2>&1");
return Response::json(['success' => true, 'output' => $output]);
} catch (\Throwable $th) {
return ResponseHelper::returnError($th->getMessage());
}
}
/**
* Run `php artisan migrate --force` (non-interactive).
*/
public function runMigrate(Request $request)
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
try {
$output = shell_exec('cd ' . escapeshellarg(BASE_PATH) . ' && php artisan migrate --force 2>&1');
return Response::json(['success' => true, 'output' => $output]);
} catch (\Throwable $th) {
return ResponseHelper::returnError($th->getMessage());
}
}
/**
* Download a full database backup.
* Puts system in maintenance mode during the process.
*/
public function downloadBackup()
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
// 1. Enable maintenance mode & Notify
Redis::set('system:maintenance_mode', 'true');
Redis::set('system:global_message', json_encode(['text' => 'System backup in progress. Transactions temporarily disabled.', 'type' => 'warning']));
try {
$filename = 'backup_' . date('Y-m-d_H-i-s') . '.sql';
$path = BASE_PATH . '/storage/app/backups/' . $filename;
if (!is_dir(dirname($path))) {
mkdir(dirname($path), 0755, true);
}
$dbHost = env('DB_HOST', '127.0.0.1');
$dbPort = env('DB_PORT', '3306');
$dbName = env('DB_DATABASE', 'bukid');
$dbUser = env('DB_USERNAME', 'root');
$dbPass = env('DB_PASSWORD', '');
$dump = new \Ifsnop\Mysqldump\Mysqldump(
"mysql:host={$dbHost};port={$dbPort};dbname={$dbName}",
$dbUser,
$dbPass,
[
'add-drop-table' => true,
'exclude-tables' => ['db_backups'] // Exclude the backups table
]
);
$dump->start($path);
if (!file_exists($path) || filesize($path) === 0) {
throw new \Exception('Backup file was not created or is empty.');
}
// Compress into 7z Ultra
$sevenZFilename = 'backup_' . date('Y-m-d_H-i-s') . '.7z';
$sevenZPath = BASE_PATH . '/storage/app/backups/' . $sevenZFilename;
// -mx=9 for Ultra compression
$path_escaped = escapeshellarg($path);
$sevenZPath_escaped = escapeshellarg($sevenZPath);
$command = "7z a -t7z -m0=lzma2 -mx=9 {$sevenZPath_escaped} {$path_escaped} 2>&1";
shell_exec($command);
if (!file_exists($sevenZPath)) {
throw new \Exception('Failed to create 7z archive.');
}
// Save to database
$fileContentRaw = file_get_contents($sevenZPath);
$fileHash = hash('sha256', $fileContentRaw);
$fileContent = new FileContent();
$fileContent->filehash = $fileHash;
$fileContent->titlename = $sevenZFilename;
$fileContent->description = 'System database backup';
$fileContent->size_in_bytes = filesize($sevenZPath);
$fileContent->content = base64_encode($fileContentRaw);
$fileContent->mimetype = 'application/x-7z-compressed';
$fileContent->created_by = Auth::id();
$fileContent->updated_by = Auth::id();
$fileContent->save();
$dbBackup = new DbBackup();
$dbBackup->file_content_hashkey = $fileContent->hashkey;
$dbBackup->filename = $sevenZFilename;
$dbBackup->size_in_bytes = filesize($sevenZPath);
$dbBackup->created_by = Auth::id();
$dbBackup->updated_by = Auth::id();
$dbBackup->save();
// Clean up the temporary files from filesystem
@unlink($path);
@unlink($sevenZPath);
// 2. Disable maintenance mode & Clear Notify
Redis::set('system:maintenance_mode', 'false');
Redis::del('system:global_message');
// 3. Return the binary content for download
return Response::make($fileContentRaw, 200, [
'Content-Type' => 'application/x-7z-compressed',
'Content-Disposition' => 'attachment; filename="' . $sevenZFilename . '"',
'Content-Length' => strlen($fileContentRaw),
]);
} catch (\Throwable $th) {
Redis::set('system:maintenance_mode', 'false');
return ResponseHelper::returnError($th->getMessage());
}
}
/**
* Get recently created backups.
*/
public function getBackups()
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$backups = DbBackup::with(['creator'])
->orderBy('created_at', 'desc')
->limit(50)
->get();
return Response::json(['success' => true, 'data' => $backups]);
}
/**
* Rename a specific backup.
*/
public function renameBackup(Request $request)
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$hash = $request->input('hash');
$newName = $request->input('name');
if (empty($newName)) return ResponseHelper::returnError('Name cannot be empty');
$backup = DbBackup::where('hashkey', $hash)->first();
if (!$backup) return ResponseHelper::returnError('Backup not found');
$backup->name = $newName;
$backup->save();
return Response::json(['success' => true]);
}
/**
* Delete a specific backup.
*/
public function deleteBackup(Request $request)
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$hash = $request->input('hash');
$backup = DbBackup::where('hashkey', $hash)->first();
if (!$backup) return ResponseHelper::returnError('Backup not found');
// Delete associated file content
FileContent::where('hashkey', $backup->file_content_hashkey)->delete();
// Delete backup record
$backup->delete();
return Response::json(['success' => true]);
}
/**
* Download a specific backup from the database.
*/
public function downloadBackupByHash(Request $request)
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$hash = $request->input('hash');
$backup = DbBackup::where('hashkey', $hash)->first();
if (!$backup) return ResponseHelper::returnError('Backup not found');
$fileContent = FileContent::where('hashkey', $backup->file_content_hashkey)->first();
if (!$fileContent) return ResponseHelper::returnError('File content not found');
$content = base64_decode($fileContent->content);
return Response::make($content, 200, [
'Content-Type' => $fileContent->mimetype,
'Content-Disposition' => 'attachment; filename="' . $backup->filename . '"',
'Content-Length' => strlen($content),
]);
}
/**
* Get system-wide logs from file and database.
*/
public function getSystemLogs(Request $request)
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$type = $request->input('type', 'database');
if ($type === 'file') {
$logPath = BASE_PATH . '/storage/logs/hypervel.log';
if (!file_exists($logPath)) {
return Response::json(['success' => true, 'data' => 'No file logs found.']);
}
$logs = shell_exec("tail -n 1000 " . escapeshellarg($logPath));
return Response::json(['success' => true, 'data' => $logs]);
}
// Database logs (audit)
if ($type === 'audit') {
$logs = DB::table('table_logs')->orderBy('id', 'desc')->limit(500)->get();
return Response::json(['success' => true, 'data' => $logs]);
}
// Database logs (system)
$logs = DB::table('logs')->orderBy('uid', 'desc')->limit(500)->get();
return Response::json(['success' => true, 'data' => $logs]);
}
}