Testing Alpha
This commit is contained in:
42
src/Application/Auth/Actions/Fortify/CreateNewUser.php
Normal file
42
src/Application/Auth/Actions/Fortify/CreateNewUser.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Auth\Actions\Fortify;
|
||||
|
||||
use Laravel\Fortify\Contracts\CreatesNewUsers;
|
||||
use Koneko\VuexyAdmin\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class CreateNewUser implements CreatesNewUsers
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
/**
|
||||
* Validate and create a newly registered user.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*/
|
||||
public function create(array $input): User
|
||||
{
|
||||
Validator::make($input, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:255',
|
||||
Rule::unique(User::class),
|
||||
],
|
||||
'password' => $this->passwordRules(),
|
||||
])->validate();
|
||||
|
||||
return User::create([
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
'password' => Hash::make($input['password']),
|
||||
]);
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Auth\Actions\Fortify;
|
||||
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
trait PasswordValidationRules
|
||||
{
|
||||
/**
|
||||
* Get the validation rules used to validate passwords.
|
||||
*
|
||||
* @return array<int, \Illuminate\Contracts\Validation\Rule|array<mixed>|string>
|
||||
*/
|
||||
protected function passwordRules(): array
|
||||
{
|
||||
return ['required', 'string', Password::default(), 'confirmed'];
|
||||
}
|
||||
}
|
30
src/Application/Auth/Actions/Fortify/ResetUserPassword.php
Normal file
30
src/Application/Auth/Actions/Fortify/ResetUserPassword.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Auth\Actions\Fortify;
|
||||
|
||||
use Illuminate\Support\Facades\{Hash,Validator};
|
||||
use Koneko\VuexyAdmin\Models\User;
|
||||
use Laravel\Fortify\Contracts\ResetsUserPasswords;
|
||||
|
||||
class ResetUserPassword implements ResetsUserPasswords
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
/**
|
||||
* Validate and reset the user's forgotten password.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*/
|
||||
public function reset(User $user, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'password' => $this->passwordRules(),
|
||||
])->validate();
|
||||
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($input['password']),
|
||||
])->save();
|
||||
}
|
||||
}
|
34
src/Application/Auth/Actions/Fortify/UpdateUserPassword.php
Normal file
34
src/Application/Auth/Actions/Fortify/UpdateUserPassword.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Auth\Actions\Fortify;
|
||||
|
||||
use Laravel\Fortify\Contracts\UpdatesUserPasswords;
|
||||
use Koneko\VuexyAdmin\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class UpdateUserPassword implements UpdatesUserPasswords
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
/**
|
||||
* Validate and update the user's password.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*/
|
||||
public function update(User $user, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'current_password' => ['required', 'string', 'current_password:web'],
|
||||
'password' => $this->passwordRules(),
|
||||
], [
|
||||
'current_password.current_password' => __('The provided password does not match your current password.'),
|
||||
])->validateWithBag('updatePassword');
|
||||
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($input['password']),
|
||||
])->save();
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Auth\Actions\Fortify;
|
||||
|
||||
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
|
||||
use Koneko\VuexyAdmin\Models\User;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
||||
{
|
||||
/**
|
||||
* Validate and update the given user's profile information.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*/
|
||||
public function update(User $user, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:255',
|
||||
Rule::unique('users')->ignore($user->id),
|
||||
],
|
||||
])->validateWithBag('updateProfileInformation');
|
||||
|
||||
if (
|
||||
$input['email'] !== $user->email &&
|
||||
$user instanceof MustVerifyEmail
|
||||
) {
|
||||
$this->updateVerifiedUser($user, $input);
|
||||
} else {
|
||||
$user->forceFill([
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the given verified user's profile information.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*/
|
||||
protected function updateVerifiedUser(User $user, array $input): void
|
||||
{
|
||||
$user->forceFill([
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
'email_verified_at' => null,
|
||||
])->save();
|
||||
|
||||
$user->sendEmailVerificationNotification();
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Bootstrap\Extenders\Api;
|
||||
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Koneko\VuexyAdmin\Application\Bootstrap\KonekoModule;
|
||||
use Koneko\VuexyApisAndIntegrations\Models\ExternalApi;
|
||||
use Illuminate\Support\Collection;
|
||||
use Koneko\VuexyAdmin\Application\Bootstrap\KonekoModuleRegistry;
|
||||
use Koneko\VuexyAdmin\Application\Bootstrap\KonekoComponentContextRegistrar;
|
||||
|
||||
class ApiModuleRegistry
|
||||
{
|
||||
/**
|
||||
* Importa APIs desde el archivo api.json de cada módulo.
|
||||
*/
|
||||
public static function importModuleApis(KonekoModule $module): void
|
||||
{
|
||||
$path = base_path($module->basePath . '/database/data/apis/api.json');
|
||||
|
||||
if (!File::exists($path)) {
|
||||
Log::info("[API Import] No se encontró archivo api.json para el módulo {$module->slug}");
|
||||
return;
|
||||
}
|
||||
|
||||
$data = json_decode(File::get($path), true);
|
||||
|
||||
if (!is_array($data)) {
|
||||
Log::warning("[API Import] Formato inválido en {$path}");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($data as $entry) {
|
||||
if (empty($entry['slug']) || empty($entry['provider']) || empty($entry['base_url'])) {
|
||||
Log::warning("[API Import] Entrada inválida: se omite", $entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
$entry['module'] = $module->componentNamespace ?? $module->slug;
|
||||
|
||||
ExternalApi::updateOrCreate(
|
||||
['slug' => $entry['slug'], 'module' => $entry['module']],
|
||||
collect($entry)->except(['slug', 'module'])->toArray()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exporta todas las APIs de un módulo al archivo api.json.
|
||||
*/
|
||||
public static function exportModuleApis(KonekoModule $module): void
|
||||
{
|
||||
$apis = ExternalApi::where('module', $module->componentNamespace ?? $module->slug)->get();
|
||||
|
||||
$path = base_path($module->basePath . '/database/data/apis/api.json');
|
||||
File::ensureDirectoryExists(dirname($path));
|
||||
|
||||
File::put($path, json_encode($apis->map(function ($api) {
|
||||
return collect($api->toArray())->except(['id', 'created_by', 'updated_by', 'created_at', 'updated_at'])->toArray();
|
||||
}), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Exporta todas las APIs del sistema por módulo.
|
||||
*/
|
||||
public static function exportAll(): void
|
||||
{
|
||||
foreach (KonekoModuleRegistry::enabled() as $module) {
|
||||
self::exportModuleApis($module);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Importa APIs de todos los módulos registrados.
|
||||
*/
|
||||
public static function importAll(): void
|
||||
{
|
||||
foreach (KonekoModuleRegistry::enabled() as $module) {
|
||||
self::importModuleApis($module);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Devuelve todas las APIs del módulo actual.
|
||||
*/
|
||||
public static function forCurrentModule(): Collection
|
||||
{
|
||||
return ExternalApi::where('module', KonekoComponentContextRegistrar::currentComponent() ?? 'unknown')->get();
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Bootstrap\Extenders\Catalog;
|
||||
|
||||
use Koneko\VuexyAdmin\Application\Contracts\Catalogs\CatalogServiceInterface;
|
||||
|
||||
/**
|
||||
* Registry centralizado de servicios de catálogos.
|
||||
*
|
||||
* Permite registrar servicios por "componente" (slug) y resolverlos desde cualquier parte.
|
||||
*/
|
||||
class CatalogModuleRegistry
|
||||
{
|
||||
/** @var array<string, class-string<CatalogServiceInterface>> */
|
||||
protected static array $registry = [];
|
||||
|
||||
/**
|
||||
* Registra un servicio de catálogo para un componente.
|
||||
*/
|
||||
public static function register(string $component, string $serviceClass): void
|
||||
{
|
||||
static::$registry[$component] = $serviceClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna una instancia del servicio de catálogo asociado al componente.
|
||||
*/
|
||||
public static function get(string $component): ?CatalogServiceInterface
|
||||
{
|
||||
if (!isset(static::$registry[$component])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return app(static::$registry[$component]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Devuelve todos los componentes registrados.
|
||||
*/
|
||||
public static function all(): array
|
||||
{
|
||||
return array_keys(static::$registry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si un componente está registrado.
|
||||
*/
|
||||
public static function has(string $component): bool
|
||||
{
|
||||
return isset(static::$registry[$component]);
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Bootstrap\Extenders\Model;
|
||||
|
||||
class ModelExtensionRegistry
|
||||
{
|
||||
protected static array $modelAttributes = [];
|
||||
protected static array $configExtensions = [];
|
||||
|
||||
// ========= REGISTRO DE ATRIBUTOS DINÁMICOS =========
|
||||
|
||||
public static function registerModelAttributes(string $modelClass, array $attributes): void
|
||||
{
|
||||
foreach ($attributes as $key => $values) {
|
||||
self::$modelAttributes[$modelClass][$key] = array_merge(
|
||||
self::$modelAttributes[$modelClass][$key] ?? [],
|
||||
$values
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getAttributesFor(string $modelClass, string $key): array
|
||||
{
|
||||
return self::$modelAttributes[$modelClass][$key] ?? [];
|
||||
}
|
||||
|
||||
// ========= REGISTRO DE EXTENSIONES DE CONFIGURADORES =========
|
||||
|
||||
public static function registerConfigExtensions(string $targetClass, array $extensions): void
|
||||
{
|
||||
self::$configExtensions[$targetClass] = array_merge(
|
||||
self::$configExtensions[$targetClass] ?? [],
|
||||
$extensions
|
||||
);
|
||||
}
|
||||
|
||||
public static function getConfigExtensionsFor(string $targetClass): array
|
||||
{
|
||||
return self::$configExtensions[$targetClass] ?? [];
|
||||
}
|
||||
|
||||
// ========= REGISTRO DE FLAGS =========
|
||||
|
||||
public static function registerModelFlags(string $modelClass, array $flags): void
|
||||
{
|
||||
foreach ($flags as $flag => $description) {
|
||||
self::$modelAttributes[$modelClass]['flags'][$flag] = $description;
|
||||
}
|
||||
}
|
||||
|
||||
public static function getFlagsFor(string $modelClass): array
|
||||
{
|
||||
return self::$modelAttributes[$modelClass]['flags'] ?? [];
|
||||
}
|
||||
}
|
100
src/Application/Bootstrap/KonekoComponentContextRegistrar.php
Normal file
100
src/Application/Bootstrap/KonekoComponentContextRegistrar.php
Normal file
@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Bootstrap;
|
||||
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Koneko\VuexyAdmin\Application\Contracts\Settings\SettingsRepositoryInterface;
|
||||
use Koneko\VuexyAdmin\Application\Cache\KonekoCacheManager;
|
||||
use Koneko\VuexyAdmin\Application\Loggers\{KonekoSystemLogger, KonekoSecurityLogger, KonekoUserInteractionLogger};
|
||||
|
||||
class KonekoComponentContextRegistrar
|
||||
{
|
||||
protected static ?string $currentComponent = null;
|
||||
protected static ?string $currentSlug = null;
|
||||
|
||||
public static function registerComponent(string $componentNamespace, ?string $slug = null): void
|
||||
{
|
||||
self::$currentComponent = $componentNamespace;
|
||||
self::$currentSlug = $slug;
|
||||
|
||||
/*
|
||||
if (App::bound(SettingsRepositoryInterface::class)) {
|
||||
App::make(SettingsRepositoryInterface::class)
|
||||
->setNamespaceByComponent($componentNamespace);
|
||||
}
|
||||
*/
|
||||
|
||||
if (class_exists(KonekoCacheManager::class)) {
|
||||
try {
|
||||
cache_manager($componentNamespace)->registerDefaults();
|
||||
} catch (\Throwable $e) {
|
||||
logger()->warning("[KonekoContext] No se pudo registrar defaults para CacheManager: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
if (class_exists(KonekoSystemLogger::class)) {
|
||||
KonekoSystemLogger::setComponent($componentNamespace, $slug);
|
||||
}
|
||||
|
||||
if (class_exists(KonekoSecurityLogger::class)) {
|
||||
KonekoSecurityLogger::setComponent($componentNamespace, $slug);
|
||||
}
|
||||
|
||||
if (class_exists(KonekoUserInteractionLogger::class)) {
|
||||
KonekoUserInteractionLogger::setComponent($componentNamespace, $slug);
|
||||
}
|
||||
|
||||
// Futuro: API Manager y Event Dispatcher
|
||||
// api_manager()->setComponent($componentNamespace);
|
||||
// event_dispatcher()->context($componentNamespace);
|
||||
}
|
||||
|
||||
public static function currentComponent(): ?string
|
||||
{
|
||||
return self::$currentComponent;
|
||||
}
|
||||
|
||||
public static function currentSlug(): ?string
|
||||
{
|
||||
return self::$currentSlug;
|
||||
}
|
||||
|
||||
public static function hasComponent(): bool
|
||||
{
|
||||
return self::$currentComponent !== null;
|
||||
}
|
||||
|
||||
public static function hasSlug(): bool
|
||||
{
|
||||
return self::$currentSlug !== null;
|
||||
}
|
||||
|
||||
public static function reset(): void
|
||||
{
|
||||
self::$currentComponent = null;
|
||||
self::$currentSlug = null;
|
||||
}
|
||||
|
||||
public static function configPrefix(): string
|
||||
{
|
||||
return 'koneko.' . (self::$currentComponent ?? 'core');
|
||||
}
|
||||
|
||||
public static function settingsKey(string $subkey): string
|
||||
{
|
||||
return self::configPrefix() . '.settings.' . $subkey;
|
||||
}
|
||||
|
||||
public static function cacheKey(string $group, string $suffix): string
|
||||
{
|
||||
return self::configPrefix() . '.' . $group . '.' . $suffix;
|
||||
}
|
||||
|
||||
public static function bootAfterBindings(): void
|
||||
{
|
||||
// api_manager()->registerFromComponentContext();
|
||||
// catalog_register_contextual();
|
||||
}
|
||||
}
|
403
src/Application/Bootstrap/KonekoModule.php
Normal file
403
src/Application/Bootstrap/KonekoModule.php
Normal file
@ -0,0 +1,403 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Bootstrap;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class KonekoModule
|
||||
{
|
||||
// === Identidad del Módulo ===
|
||||
public ?string $vendor;
|
||||
public string $name;
|
||||
public string $slug;
|
||||
public string $description;
|
||||
public string $type;
|
||||
public array $tags;
|
||||
public string $version;
|
||||
public array $keywords;
|
||||
public array $authors;
|
||||
public ?array $support;
|
||||
public ?string $license;
|
||||
public string $minimumStability;
|
||||
public string $buildVersion;
|
||||
|
||||
// === Namespace de configuraciones ===
|
||||
public string $componentNamespace;
|
||||
|
||||
// === Datos de composer/autoload ===
|
||||
public string $composerName;
|
||||
public string $namespace;
|
||||
public ?string $provider;
|
||||
public string $basePath;
|
||||
public string $composerPath;
|
||||
public array $dependencies;
|
||||
|
||||
// === Metadatos visuales para UI del gestor ===
|
||||
public array $ui;
|
||||
|
||||
// === Archivos de configuraciones ===
|
||||
public array $configs;
|
||||
|
||||
// === Providers, Middleware y Aliases (runtime) ===
|
||||
public array $providers;
|
||||
public array $middleware;
|
||||
public array $aliases;
|
||||
|
||||
// === Singleton *Nuevo Prototipo ===
|
||||
public array $singletons;
|
||||
|
||||
// === Bindings de interfaces a servicios ===
|
||||
public array $bindings;
|
||||
|
||||
// === Macro ===
|
||||
public array $macros;
|
||||
|
||||
// === Observadores y Auditable ===
|
||||
public array $observers;
|
||||
public array $listeners;
|
||||
public array $auditable;
|
||||
|
||||
// === Migraciones ===
|
||||
public array $migrations;
|
||||
|
||||
// === Rutas ===
|
||||
public array $routes;
|
||||
|
||||
// === Vistas y traducciones ===
|
||||
public array $views;
|
||||
public array $translations;
|
||||
|
||||
// === Livewire, Blade, Vistas ===
|
||||
public array $bladeComponents;
|
||||
public array $livewire;
|
||||
|
||||
// === Archivos publicables ===
|
||||
public array $publishedFiles;
|
||||
|
||||
// === Comandos Artisan ===
|
||||
public array $commands;
|
||||
|
||||
public array $schedules;
|
||||
|
||||
// === Roles y permisos ===
|
||||
public array $rbac;
|
||||
|
||||
// === APIs ===
|
||||
public array $apis;
|
||||
|
||||
// === Catálogos ===
|
||||
public array $catalogs;
|
||||
|
||||
// === Extensiones del ecosistema ===
|
||||
public array $extensions;
|
||||
|
||||
public function __construct(array $overrides = [])
|
||||
{
|
||||
$this->vendor = $overrides['vendor'] ?? null;
|
||||
$this->name = $overrides['name'] ?? '';
|
||||
$this->slug = $overrides['slug'] ?? '';
|
||||
$this->description = $overrides['description'] ?? '';
|
||||
$this->type = $overrides['type'] ?? 'plugin';
|
||||
$this->tags = $overrides['tags'] ?? [];
|
||||
$this->version = $overrides['version'] ?? '1.0.0';
|
||||
$this->keywords = $overrides['keywords'] ?? [];
|
||||
$this->authors = $overrides['authors'] ?? [];
|
||||
$this->support = $overrides['support'] ?? [];
|
||||
$this->license = $overrides['license'] ?? null;
|
||||
$this->minimumStability= $overrides['minimumStability']?? 'stable';
|
||||
$this->buildVersion = $overrides['buildVersion'] ?? now()->format('YmdHis');
|
||||
|
||||
$this->componentNamespace = $overrides['componentNamespace'] ?? '';
|
||||
|
||||
$this->composerName = $overrides['composerName'] ?? '';
|
||||
$this->namespace = $overrides['namespace'] ?? '';
|
||||
$this->provider = $overrides['provider'] ?? null;
|
||||
$this->basePath = $overrides['basePath'] ?? '';
|
||||
$this->composerPath = $overrides['composerPath'] ?? '';
|
||||
$this->dependencies = $overrides['dependencies'] ?? [];
|
||||
|
||||
$this->ui = $overrides['ui'] ?? [];
|
||||
|
||||
$this->configs = $overrides['configs'] ?? [];
|
||||
|
||||
$this->providers = $overrides['providers'] ?? [];
|
||||
$this->middleware = $overrides['middleware'] ?? [];
|
||||
$this->aliases = $overrides['aliases'] ?? [];
|
||||
|
||||
$this->singletons = $overrides['singletons'] ?? [];
|
||||
$this->bindings = $overrides['bindings'] ?? [];
|
||||
$this->macros = $overrides['macros'] ?? [];
|
||||
|
||||
$this->observers = $overrides['observers'] ?? [];
|
||||
$this->listeners = $overrides['listeners'] ?? [];
|
||||
$this->auditable = $overrides['auditable'] ?? [];
|
||||
|
||||
$this->migrations = $overrides['migrations'] ?? [];
|
||||
|
||||
$this->routes = $overrides['routes'] ?? [];
|
||||
|
||||
$this->views = $overrides['views'] ?? [];
|
||||
$this->translations = $overrides['translations'] ?? [];
|
||||
|
||||
$this->bladeComponents = $overrides['bladeComponents'] ?? [];
|
||||
$this->livewire = $overrides['livewire'] ?? [];
|
||||
|
||||
$this->publishedFiles = $overrides['publishedFiles'] ?? [];
|
||||
|
||||
$this->commands = $overrides['commands'] ?? [];
|
||||
$this->schedules = $overrides['schedules'] ?? [];
|
||||
|
||||
$this->rbac = $overrides['rbac'] ?? [];
|
||||
$this->apis = $overrides['apis'] ?? [];
|
||||
$this->catalogs = $overrides['catalogs'] ?? [];
|
||||
|
||||
$this->extensions = $overrides['extensions'] ?? [];
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return Str::slug($this->composerName);
|
||||
}
|
||||
|
||||
public function hasTag(string $tag): bool
|
||||
{
|
||||
return in_array($tag, $this->tags, true);
|
||||
}
|
||||
|
||||
public function toSummary(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'slug' => $this->slug,
|
||||
'description' => $this->description,
|
||||
'version' => $this->version,
|
||||
'type' => $this->type,
|
||||
'tags' => $this->tags,
|
||||
];
|
||||
}
|
||||
|
||||
public function getDetails(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'slug' => $this->slug,
|
||||
'description' => $this->description,
|
||||
'version' => $this->version,
|
||||
'type' => $this->type,
|
||||
'keywords' => $this->keywords,
|
||||
'tags' => $this->tags,
|
||||
'authors' => $this->authors,
|
||||
'namespace' => $this->namespace,
|
||||
'composerName' => $this->composerName,
|
||||
'provider' => $this->provider,
|
||||
'basePath' => $this->basePath,
|
||||
'composerPath' => $this->composerPath,
|
||||
'dependencies' => $this->dependencies,
|
||||
];
|
||||
}
|
||||
|
||||
public function getVisualDescriptor(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->getId(),
|
||||
'slug' => $this->slug,
|
||||
'title' => Str::title(str_replace('-', ' ', $this->slug)),
|
||||
'description' => $this->description,
|
||||
'version' => $this->version,
|
||||
'buildVersion' => $this->buildVersion,
|
||||
'type' => $this->type,
|
||||
'vendor' => $this->vendor ?? null,
|
||||
'license' => $this->license ?? 'proprietary',
|
||||
'minimumStability' => $this->minimumStability ?? 'stable',
|
||||
'tags' => $this->tags,
|
||||
'authors' => $this->authors,
|
||||
'support' => $this->support,
|
||||
'icon' => $this->ui['icon'] ?? 'ti ti-box',
|
||||
'color' => $this->ui['color'] ?? 'primary',
|
||||
'image' => $this->ui['image'] ?? null,
|
||||
'readme_path' => $this->ui['readme'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
private static function buildAttributesFromComposer(array $composer, string $basePath, array $custom = []): array
|
||||
{
|
||||
$nameParts = explode('/', $composer['name'] ?? 'unknown/module');
|
||||
$vendor = $nameParts[0] ?? 'unknown';
|
||||
$slug = $nameParts[1] ?? 'module';
|
||||
|
||||
return [
|
||||
'vendor' => $vendor,
|
||||
'slug' => $slug,
|
||||
'name' => $custom['name'] ?? 'unknown/module',
|
||||
'description' => $custom['description'] ?? ($composer['description'] ?? ''),
|
||||
'type' => $custom['type'] ?? ($composer['type'] ?? 'plugin'),
|
||||
'tags' => $custom['tags'] ?? ($composer['keywords'] ?? []),
|
||||
'composerName' => $composer['name'] ?? 'unknown/module',
|
||||
'version' => $composer['version'] ?? '1.0.0',
|
||||
'keywords' => $composer['keywords'] ?? [],
|
||||
'authors' => $composer['authors'] ?? [],
|
||||
'support' => $composer['support'] ?? [],
|
||||
'license' => $composer['license'] ?? 'proprietary',
|
||||
'minimumStability' => $composer['minimum-stability'] ?? 'stable',
|
||||
'buildVersion' => now()->format('YmdHis'), // Versión de compilación en timestamp
|
||||
'namespace' => array_key_first($composer['autoload']['psr-4'] ?? []) ?? '',
|
||||
'provider' => $composer['extra']['laravel']['providers'][0] ?? null,
|
||||
'basePath' => rtrim($basePath, '/'),
|
||||
'composerPath' => rtrim($basePath, '/') . '/composer.json',
|
||||
'dependencies' => array_keys($composer['require'] ?? []),
|
||||
'ui' => $custom['ui'] ?? [],
|
||||
'configs' => $custom['configs'] ?? [],
|
||||
'componentNamespace' => $custom['componentNamespace'] ?? '',
|
||||
'providers' => $custom['providers'] ?? [],
|
||||
'middleware' => $custom['middleware'] ?? [],
|
||||
'aliases' => $custom['aliases'] ?? [],
|
||||
'singletons' => $custom['singletons'] ?? [],
|
||||
'bindings' => $custom['bindings'] ?? [],
|
||||
'macros' => $custom['macros'] ?? [],
|
||||
'observers' => $custom['observers'] ?? [],
|
||||
'listeners' => $custom['listeners'] ?? [],
|
||||
'auditable' => $custom['auditable'] ?? [],
|
||||
'migrations' => $custom['migrations'] ?? [],
|
||||
'routes' => $custom['routes'] ?? [],
|
||||
'views' => $custom['views'] ?? [],
|
||||
'translations' => $custom['translations'] ?? [],
|
||||
'bladeComponents' => $custom['bladeComponents'] ?? [],
|
||||
'livewire' => $custom['livewire'] ?? [],
|
||||
'publishedFiles' => $custom['publishedFiles'] ?? [],
|
||||
'commands' => $custom['commands'] ?? [],
|
||||
'schedules' => $custom['schedules'] ?? [],
|
||||
'rbac' => $custom['rbac'] ?? [],
|
||||
'apis' => $custom['apis'] ?? [],
|
||||
'catalogs' => $custom['catalogs'] ?? [],
|
||||
'extensions' => $custom['extensions'] ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromModuleDirectory(string $dirPath): ?KonekoModule
|
||||
{
|
||||
$file = null;
|
||||
|
||||
if (file_exists($dirPath . '/vuexy-admin.module.php')){
|
||||
$file = $dirPath . '/vuexy-admin.module.php';
|
||||
}
|
||||
|
||||
if (file_exists($dirPath . '/koneko-vuexy.module.php')){
|
||||
$file = $dirPath . '/koneko-vuexy.module.php';
|
||||
}
|
||||
|
||||
$result = require $file;
|
||||
|
||||
if ($result instanceof KonekoModule) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if (is_array($result)) {
|
||||
// Inferir basePath desde el archivo
|
||||
$basePath = dirname($file, 2);
|
||||
|
||||
return self::fromModuleDefinition($basePath, $result);
|
||||
}
|
||||
|
||||
throw new \RuntimeException("El archivo [$file] no contiene una instancia ni un array válido para definir el módulo.");
|
||||
}
|
||||
|
||||
public static function fromModuleDefinition(string $basePath, array $custom = []): self
|
||||
{
|
||||
$composerPath = rtrim($basePath, '/') . '/composer.json';
|
||||
|
||||
if (!file_exists($composerPath)) {
|
||||
throw new \RuntimeException("No se encontró composer.json en: {$composerPath}");
|
||||
}
|
||||
|
||||
$composer = json_decode(file_get_contents($composerPath), true);
|
||||
|
||||
if (!is_array($composer)) {
|
||||
throw new \RuntimeException("composer.json inválido en: {$composerPath}");
|
||||
}
|
||||
|
||||
$attributes = static::buildAttributesFromComposer($composer, realpath($basePath), $custom);
|
||||
|
||||
$module = new self($attributes);
|
||||
|
||||
return $module;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public static function fromComposerJson(array $composer, string $basePath, array $custom = []): self
|
||||
{
|
||||
return new self(
|
||||
static::buildAttributesFromComposer($composer, $basePath, $custom)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra desde archivo PHP que retorna un KonekoModule.
|
||||
*/
|
||||
public static function fromModuleFile(string $file): ?KonekoModule
|
||||
{
|
||||
if (!file_exists($file)) return null;
|
||||
|
||||
$result = require $file;
|
||||
|
||||
if ($result instanceof KonekoModule) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if (is_array($result)) {
|
||||
// Inferir basePath desde el archivo
|
||||
$basePath = dirname($file, 2);
|
||||
|
||||
return self::fromModuleDefinition($basePath, $result);
|
||||
}
|
||||
|
||||
throw new \RuntimeException("El archivo [$file] no contiene una instancia ni un array válido para definir el módulo.");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Obtiene el slug del módulo actualmente activo, basado en la ruta.
|
||||
*/
|
||||
public static function currentSlug(): string
|
||||
{
|
||||
$module = static::resolveCurrent();
|
||||
|
||||
return $module?->slug ?? 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el módulo actual, basado en la ruta.
|
||||
*
|
||||
* @return static|null
|
||||
*/
|
||||
public static function resolveCurrent(): ?self
|
||||
{
|
||||
$modules = KonekoModuleRegistry::enabled();
|
||||
|
||||
if (empty($modules)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$currentRoute = request()?->route()?->getName();
|
||||
|
||||
foreach ($modules as $module) {
|
||||
if (str_contains($currentRoute, $module->slug)) {
|
||||
return $module;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: el primero que esté activo
|
||||
return reset($modules) ?: null;
|
||||
}
|
||||
}
|
270
src/Application/Bootstrap/KonekoModuleBootManager.php
Normal file
270
src/Application/Bootstrap/KonekoModuleBootManager.php
Normal file
@ -0,0 +1,270 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Bootstrap;
|
||||
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\AliasLoader;
|
||||
use Illuminate\Routing\Router;
|
||||
use Illuminate\Support\Facades\{App,Blade,Event,Lang,Route,View};
|
||||
use Koneko\VuexyAdmin\Application\Bootstrap\Extenders\Catalog\CatalogModuleRegistry;
|
||||
use Koneko\VuexyAdmin\Application\Factories\FactoryExtensionRegistry;
|
||||
use Koneko\VuexyAdmin\Application\Bootstrap\Extenders\Model\ModelExtensionRegistry;
|
||||
use Livewire\Livewire;
|
||||
|
||||
class KonekoModuleBootManager
|
||||
{
|
||||
public static function bootAll(): void
|
||||
{
|
||||
$modulesActivated = [];
|
||||
|
||||
foreach (KonekoModuleRegistry::enabled() as $module) {
|
||||
static::boot($module);
|
||||
$modulesActivated[] = $module->slug;
|
||||
}
|
||||
}
|
||||
|
||||
public static function boot(KonekoModule $module): void
|
||||
{
|
||||
// ⚙️ Archivos de configuración del módulo
|
||||
foreach ($module->configs ?? [] as $namespace => $relativePath) {
|
||||
$fullPath = $module->basePath . DIRECTORY_SEPARATOR . $relativePath;
|
||||
|
||||
if (file_exists($fullPath)) {
|
||||
$config = require $fullPath;
|
||||
|
||||
if (is_array($config)) {
|
||||
config()->set($namespace, array_merge(
|
||||
config($namespace, []),
|
||||
$config
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 🛡️ Middleware
|
||||
foreach ($module->middleware ?? [] as $alias => $middlewareClass) {
|
||||
// @var Router $router
|
||||
$router = app(Router::class);
|
||||
$router->aliasMiddleware($alias, $middlewareClass);
|
||||
}
|
||||
|
||||
// 🏭 Proveedores de servicio
|
||||
foreach ($module->providers ?? [] as $provider) {
|
||||
App::register($provider);
|
||||
}
|
||||
|
||||
// 🧩 Alias de clases
|
||||
foreach ($module->aliases ?? [] as $alias => $class) {
|
||||
AliasLoader::getInstance()->alias($alias, $class);
|
||||
}
|
||||
|
||||
// 🔩 Singletons
|
||||
foreach ($model->Singletons?? [] as $singletone) {
|
||||
app()->singleton($singletone);
|
||||
}
|
||||
|
||||
// 🔗 Bindings de interfaces a servicios
|
||||
foreach ($module->bindings ?? [] as $abstract => $concrete) {
|
||||
app()->singleton($abstract, $concrete);
|
||||
}
|
||||
|
||||
// ⚙️ Namespace de componentes
|
||||
if (!empty($module->componentNamespace)) {
|
||||
KonekoComponentContextRegistrar::registerComponent($module->componentNamespace);
|
||||
}
|
||||
|
||||
// 📜 Macros
|
||||
foreach ($module->macros ?? [] as $macroPath) {
|
||||
$fullPath = static::resolvePath($module, $macroPath);
|
||||
|
||||
if (file_exists($fullPath)) {
|
||||
require_once $fullPath;
|
||||
|
||||
} else {
|
||||
logger()->warning("[KonekoModuleBootManager] ⚠️ Archivo de macro no encontrado: $fullPath");
|
||||
}
|
||||
}
|
||||
|
||||
// 🔊 Eventos
|
||||
foreach ($module->listeners ?? [] as $event => $listener) {
|
||||
Event::listen($event, $listener);
|
||||
}
|
||||
|
||||
// 🔍 Observadores
|
||||
foreach ($module->observers ?? [] as $model => $observers) {
|
||||
if (!class_exists($model)) {
|
||||
logger()->warning("🔍 Modelo no encontrado para observers: $model");
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ((array) $observers as $observer) {
|
||||
if (class_exists($observer)) {
|
||||
$model::observe($observer);
|
||||
} else {
|
||||
logger()->warning("⚠️ Observer no válido: {$observer}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 🧪 Modelos auditables
|
||||
foreach ($module->auditable ?? [] as $model) {
|
||||
if (class_exists($model)) {
|
||||
$model::observe(\OwenIt\Auditing\AuditableObserver::class);
|
||||
}
|
||||
}
|
||||
|
||||
// 🧬 Migraciones
|
||||
foreach ($module->migrations ?? [] as $relativePath) {
|
||||
$fullPath = static::resolvePath($module, $relativePath);
|
||||
|
||||
if (is_dir($fullPath)) {
|
||||
app()->make('migrator')->path($fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
// 🗺️ Rutas (después de setModule)
|
||||
foreach ($module->routes ?? [] as $routeGroup) {
|
||||
$middleware = $routeGroup['middleware'] ?? [];
|
||||
$paths = $routeGroup['paths'] ?? [];
|
||||
|
||||
foreach ($paths as $relativePath) {
|
||||
$fullPath = static::resolvePath($module, $relativePath);
|
||||
|
||||
if (file_exists($fullPath)) {
|
||||
Route::middleware($middleware)->group(function () use ($fullPath) {
|
||||
require $fullPath;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 🗂️ Vistas
|
||||
foreach ($module->views ?? [] as $namespace => $path) {
|
||||
View::addNamespace($namespace, static::resolvePath($module, $path));
|
||||
}
|
||||
|
||||
// 🌍 Traducciones
|
||||
foreach ($module->translations ?? [] as $namespace => $path) {
|
||||
Lang::addNamespace($namespace, static::resolvePath($module, $path));
|
||||
}
|
||||
|
||||
// 🧩 Componentes Blade
|
||||
foreach ($module->bladeComponents ?? [] as $prefix => $namespace) {
|
||||
Blade::componentNamespace($namespace, $prefix);
|
||||
}
|
||||
|
||||
// ⚡ Livewire
|
||||
foreach ($module->livewire ?? [] as $namespace => $components) {
|
||||
foreach ($components as $alias => $class) {
|
||||
Livewire::component("{$namespace}::{$alias}", $class);
|
||||
}
|
||||
}
|
||||
|
||||
// 🛠 Comandos Artisan
|
||||
if (App::runningInConsole() && !empty($module->commands)) {
|
||||
static::registerCommands($module->commands);
|
||||
}
|
||||
|
||||
// 🔗 Registro de APIs disponibles en el módulo
|
||||
if (!empty($module->apis['catalog_path'])) {
|
||||
$apiCatalogPath = static::resolvePath($module, $module->apis['catalog_path']);
|
||||
|
||||
if (file_exists($apiCatalogPath)) {
|
||||
$apiDefinitions = require $apiCatalogPath;
|
||||
|
||||
config()->set("apis.{$module->slug}", $apiDefinitions);
|
||||
}
|
||||
}
|
||||
|
||||
// 📑 Registro de catálogos
|
||||
foreach ($module->catalogs ?? [] as $catalog => $service) {
|
||||
CatalogModuleRegistry::register($catalog, $service);
|
||||
}
|
||||
|
||||
// 🧠 Extensiones de Modelos
|
||||
foreach ($module->extensions['config'] ?? [] as $builder => $extensions) {
|
||||
ModelExtensionRegistry::registerConfigExtensions($builder, $extensions);
|
||||
}
|
||||
|
||||
// 🧪 Extensiones de Flags de Modelos
|
||||
foreach ($module->extensions['model_flags'] ?? [] as $model => $flags) {
|
||||
ModelExtensionRegistry::registerModelFlags($model, $flags);
|
||||
}
|
||||
|
||||
// 🧬 Extensiones de Traits de Factory
|
||||
foreach ($module->extensions['factory_traits'] ?? [] as $factory => $traits) {
|
||||
foreach ((array) $traits as $trait) {
|
||||
FactoryExtensionRegistry::registerFactoryTrait($factory, $trait);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resuelve una ruta relativa al root del módulo.
|
||||
*/
|
||||
public static function resolvePath(KonekoModule $module, ?string $relativePath): string
|
||||
{
|
||||
return $module->basePath . '/' . $relativePath;
|
||||
}
|
||||
|
||||
/*
|
||||
* Registra comandos
|
||||
*/
|
||||
public static function registerCommands(array $commands): void
|
||||
{
|
||||
foreach ($commands as $command) {
|
||||
if (class_exists($command)) {
|
||||
\Illuminate\Console\Application::starting(function ($artisan) use ($command) {
|
||||
$artisan->add(App::make($command));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function scheduleAll(Schedule $schedule): void
|
||||
{
|
||||
foreach (KonekoModuleRegistry::enabled() as $module) {
|
||||
static::schedule($schedule, $module);
|
||||
}
|
||||
}
|
||||
|
||||
public static function schedule(Schedule $schedule, KonekoModule $module): void
|
||||
{
|
||||
foreach ($module->schedules ?? [] as $entry) {
|
||||
$jobClass = $entry['job'] ?? null;
|
||||
$method = $entry['method'] ?? null;
|
||||
$params = $entry['params'] ?? [];
|
||||
$chain = $entry['chain'] ?? [];
|
||||
|
||||
if (!class_exists($jobClass)) {
|
||||
logger()->warning("[VuexySchedule] ❌ Job no encontrado: {$jobClass}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Crea el evento del Job
|
||||
$event = $schedule->job(new $jobClass());
|
||||
|
||||
// Intenta aplicar el método al evento, no al schedule
|
||||
if ($method && method_exists($event, $method)) {
|
||||
$event = $event->{$method}(...$params);
|
||||
} else {
|
||||
logger()->warning("[VuexySchedule] ❌ Método inválido para evento: {$method}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Encadenamientos adicionales
|
||||
foreach ($chain as $chainMethod) {
|
||||
if (method_exists($event, $chainMethod)) {
|
||||
$event = $event->{$chainMethod}();
|
||||
} else {
|
||||
logger()->warning("[VuexySchedule] ⚠️ Método de chain no válido: {$chainMethod} en {$jobClass}");
|
||||
}
|
||||
}
|
||||
|
||||
logger()->debug("[VuexySchedule] ⏱️ Job programado correctamente: {$jobClass} usando {$method}()");
|
||||
}
|
||||
}
|
||||
}
|
153
src/Application/Bootstrap/KonekoModuleRegistry.php
Normal file
153
src/Application/Bootstrap/KonekoModuleRegistry.php
Normal file
@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Bootstrap;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Koneko\VuexyAdmin\Application\Bootstrap\KonekoModule;
|
||||
|
||||
class KonekoModuleRegistry
|
||||
{
|
||||
/** @var KonekoModule[] */
|
||||
protected static array $modules = [];
|
||||
|
||||
/**
|
||||
* Registra un módulo completo.
|
||||
*/
|
||||
public static function register(KonekoModule $module): void
|
||||
{
|
||||
static::$modules[$module->componentNamespace] = $module;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si un módulo está registrado.
|
||||
*/
|
||||
public static function has(string $name): bool
|
||||
{
|
||||
return isset(static::$modules[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Elimina un módulo del registro.
|
||||
*/
|
||||
public static function unregister(string $name): void
|
||||
{
|
||||
unset(static::$modules[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Devuelve todos los módulos registrados.
|
||||
*/
|
||||
public static function all(): array
|
||||
{
|
||||
return static::$modules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Devuelve todos los módulos como Collection.
|
||||
*/
|
||||
public static function asCollection(): Collection
|
||||
{
|
||||
return collect(static::$modules);
|
||||
}
|
||||
|
||||
/**
|
||||
* Devuelve un módulo por nombre.
|
||||
*/
|
||||
public static function get(string $name): ?KonekoModule
|
||||
{
|
||||
return static::$modules[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtra módulos por tag.
|
||||
*/
|
||||
public static function withTag(string $tag): array
|
||||
{
|
||||
return static::asCollection()
|
||||
->filter(fn(KonekoModule $m) => $m->hasTag($tag))
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Módulos con la bandera `enabled = true` (en el futuro para UI).
|
||||
*/
|
||||
public static function enabled(): array
|
||||
{
|
||||
return static::asCollection()
|
||||
->filter(fn(KonekoModule $m) => $m->enabled ?? true)
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Módulos con la bandera `enabled = false`.
|
||||
*/
|
||||
public static function disabled(): array
|
||||
{
|
||||
return static::asCollection()
|
||||
->filter(fn(KonekoModule $m) => !($m->enabled ?? true))
|
||||
->all();
|
||||
}
|
||||
|
||||
public static function discoverModule(string $basePath, int $maxDepth = 3): ?KonekoModule
|
||||
{
|
||||
$targetFiles = ['src/vuexy-admin.module.php', 'src/koneko-vuexy.module.php'];
|
||||
$level = 0;
|
||||
$dirs = [$basePath];
|
||||
|
||||
while ($level++ <= $maxDepth && !empty($dirs)) {
|
||||
$nextDirs = [];
|
||||
|
||||
foreach ($dirs as $dir) {
|
||||
foreach ($targetFiles as $relativePath) {
|
||||
$fullPath = rtrim($dir, '/') . '/' . $relativePath;
|
||||
|
||||
if (file_exists($fullPath)) {
|
||||
$module = KonekoModule::fromModuleFile($fullPath);
|
||||
|
||||
if ($module instanceof KonekoModule) {
|
||||
KonekoModuleRegistry::register($module);
|
||||
return $module;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Agregamos subdirectorios para el próximo nivel
|
||||
foreach (scandir($dir) as $entry) {
|
||||
if ($entry === '.' || $entry === '..') continue;
|
||||
|
||||
$subPath = $dir . '/' . $entry;
|
||||
if (is_dir($subPath)) {
|
||||
$nextDirs[] = $subPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$dirs = $nextDirs;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapa resumido para tarjetas en UI.
|
||||
*/
|
||||
public static function getSummaries(): array
|
||||
{
|
||||
return static::asCollection()
|
||||
->map(fn(KonekoModule $m) => $m->toSummary())
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Detalle completo de módulos (modo debug o exportación).
|
||||
*/
|
||||
public static function debugDump(): array
|
||||
{
|
||||
return static::asCollection()
|
||||
->map(fn(KonekoModule $m) => $m->getDetails())
|
||||
->all();
|
||||
}
|
||||
}
|
301
src/Application/Cache/CacheConfigService.php
Normal file
301
src/Application/Cache/CacheConfigService.php
Normal file
@ -0,0 +1,301 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Cache;
|
||||
|
||||
use Illuminate\Support\Facades\{Config,DB,Redis};
|
||||
use Memcached;
|
||||
|
||||
/**
|
||||
* Servicio para gestionar y obtener información de configuración del sistema de caché.
|
||||
*
|
||||
* Esta clase proporciona métodos para obtener información detallada sobre las configuraciones
|
||||
* de caché, sesión, base de datos y drivers del sistema. Permite consultar versiones,
|
||||
* estados y configuraciones de diferentes servicios como Redis, Memcached y bases de datos.
|
||||
*/
|
||||
class CacheConfigService
|
||||
{
|
||||
/**
|
||||
* Obtiene la configuración completa del sistema de caché y servicios relacionados.
|
||||
*
|
||||
* @return array Configuración completa que incluye caché, sesión, base de datos y drivers
|
||||
*/
|
||||
public function getConfig(): array
|
||||
{
|
||||
return [
|
||||
'cache' => $this->getCacheConfig(),
|
||||
'session' => $this->getSessionConfig(),
|
||||
'database' => $this->getDatabaseConfig(),
|
||||
'driver' => $this->getDriverVersion(),
|
||||
'memcachedInUse' => $this->isDriverInUse('memcached'),
|
||||
'redisInUse' => $this->isDriverInUse('redis'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene la configuración específica del sistema de caché.
|
||||
*
|
||||
* @return array Configuración del caché incluyendo driver, host y base de datos
|
||||
*/
|
||||
private function getCacheConfig(): array
|
||||
{
|
||||
$cacheConfig = Config::get('cache');
|
||||
$driver = $cacheConfig['default'];
|
||||
|
||||
switch ($driver) {
|
||||
case 'redis':
|
||||
$connection = config('database.redis.cache');
|
||||
$cacheConfig['host'] = $connection['host'] ?? 'localhost';
|
||||
$cacheConfig['database'] = $connection['database'] ?? 'N/A';
|
||||
break;
|
||||
|
||||
case 'database':
|
||||
$connection = config('database.connections.' . config('cache.stores.database.connection'));
|
||||
$cacheConfig['host'] = $connection['host'] ?? 'localhost';
|
||||
$cacheConfig['database'] = $connection['database'] ?? 'N/A';
|
||||
break;
|
||||
|
||||
case 'memcached':
|
||||
$servers = config('cache.stores.memcached.servers');
|
||||
$cacheConfig['host'] = $servers[0]['host'] ?? 'localhost';
|
||||
$cacheConfig['database'] = 'N/A';
|
||||
break;
|
||||
|
||||
case 'file':
|
||||
$cacheConfig['host'] = storage_path('framework/cache/data');
|
||||
$cacheConfig['database'] = 'N/A';
|
||||
break;
|
||||
|
||||
default:
|
||||
$cacheConfig['host'] = 'N/A';
|
||||
$cacheConfig['database'] = 'N/A';
|
||||
break;
|
||||
}
|
||||
|
||||
return $cacheConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene la configuración del sistema de sesiones.
|
||||
*
|
||||
* @return array Configuración de sesiones incluyendo driver, host y base de datos
|
||||
*/
|
||||
private function getSessionConfig(): array
|
||||
{
|
||||
$sessionConfig = Config::get('session');
|
||||
$driver = $sessionConfig['driver'];
|
||||
|
||||
switch ($driver) {
|
||||
case 'redis':
|
||||
$connection = config('database.redis.sessions');
|
||||
$sessionConfig['host'] = $connection['host'] ?? 'localhost';
|
||||
$sessionConfig['database'] = $connection['database'] ?? 'N/A';
|
||||
break;
|
||||
|
||||
case 'database':
|
||||
$connection = config('database.connections.' . $sessionConfig['connection']);
|
||||
$sessionConfig['host'] = $connection['host'] ?? 'localhost';
|
||||
$sessionConfig['database'] = $connection['database'] ?? 'N/A';
|
||||
break;
|
||||
|
||||
case 'memcached':
|
||||
$servers = config('cache.stores.memcached.servers');
|
||||
$sessionConfig['host'] = $servers[0]['host'] ?? 'localhost';
|
||||
$sessionConfig['database'] = 'N/A';
|
||||
break;
|
||||
|
||||
case 'file':
|
||||
$sessionConfig['host'] = storage_path('framework/sessions');
|
||||
$sessionConfig['database'] = 'N/A';
|
||||
break;
|
||||
|
||||
default:
|
||||
$sessionConfig['host'] = 'N/A';
|
||||
$sessionConfig['database'] = 'N/A';
|
||||
break;
|
||||
}
|
||||
|
||||
return $sessionConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene la configuración de la base de datos principal.
|
||||
*
|
||||
* @return array Configuración de la base de datos incluyendo host y nombre de la base de datos
|
||||
*/
|
||||
private function getDatabaseConfig(): array
|
||||
{
|
||||
$databaseConfig = Config::get('database');
|
||||
$connection = $databaseConfig['default'];
|
||||
|
||||
$connectionConfig = config('database.connections.' . $connection);
|
||||
$databaseConfig['host'] = $connectionConfig['host'] ?? 'localhost';
|
||||
$databaseConfig['database'] = $connectionConfig['database'] ?? 'N/A';
|
||||
|
||||
return $databaseConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene información sobre las versiones de los drivers en uso.
|
||||
*
|
||||
* Recopila información detallada sobre las versiones de los drivers de base de datos,
|
||||
* Redis y Memcached si están en uso en el sistema.
|
||||
*
|
||||
* @return array Información de versiones de los drivers activos
|
||||
*/
|
||||
private function getDriverVersion(): array
|
||||
{
|
||||
$drivers = [];
|
||||
$defaultDatabaseDriver = config('database.default'); // Obtén el driver predeterminado
|
||||
|
||||
switch ($defaultDatabaseDriver) {
|
||||
case 'mysql':
|
||||
case 'mariadb':
|
||||
$drivers['mysql'] = [
|
||||
'version' => $this->getMySqlVersion(),
|
||||
'details' => config("database.connections.$defaultDatabaseDriver"),
|
||||
];
|
||||
|
||||
$drivers['mariadb'] = $drivers['mysql'];
|
||||
|
||||
case 'pgsql':
|
||||
$drivers['pgsql'] = [
|
||||
'version' => $this->getPgSqlVersion(),
|
||||
'details' => config("database.connections.pgsql"),
|
||||
];
|
||||
break;
|
||||
|
||||
case 'sqlsrv':
|
||||
$drivers['sqlsrv'] = [
|
||||
'version' => $this->getSqlSrvVersion(),
|
||||
'details' => config("database.connections.sqlsrv"),
|
||||
];
|
||||
break;
|
||||
|
||||
default:
|
||||
$drivers['unknown'] = [
|
||||
'version' => 'No disponible',
|
||||
'details' => 'Driver no identificado',
|
||||
];
|
||||
break;
|
||||
}
|
||||
|
||||
// Opcional: Agrega detalles de Redis y Memcached si están en uso
|
||||
if ($this->isDriverInUse('redis')) {
|
||||
$drivers['redis'] = [
|
||||
'version' => $this->getRedisVersion(),
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->isDriverInUse('memcached')) {
|
||||
$drivers['memcached'] = [
|
||||
'version' => $this->getMemcachedVersion(),
|
||||
];
|
||||
}
|
||||
|
||||
return $drivers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene la versión del servidor MySQL.
|
||||
*
|
||||
* @return string Versión del servidor MySQL o mensaje de error
|
||||
*/
|
||||
private function getMySqlVersion(): string
|
||||
{
|
||||
try {
|
||||
$version = DB::selectOne('SELECT VERSION() as version');
|
||||
return $version->version ?? 'No disponible';
|
||||
} catch (\Exception $e) {
|
||||
return 'Error: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene la versión del servidor PostgreSQL.
|
||||
*
|
||||
* @return string Versión del servidor PostgreSQL o mensaje de error
|
||||
*/
|
||||
private function getPgSqlVersion(): string
|
||||
{
|
||||
try {
|
||||
$version = DB::selectOne("SHOW server_version");
|
||||
return $version->server_version ?? 'No disponible';
|
||||
} catch (\Exception $e) {
|
||||
return 'Error: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene la versión del servidor SQL Server.
|
||||
*
|
||||
* @return string Versión del servidor SQL Server o mensaje de error
|
||||
*/
|
||||
private function getSqlSrvVersion(): string
|
||||
{
|
||||
try {
|
||||
$version = DB::selectOne("SELECT @@VERSION as version");
|
||||
return $version->version ?? 'No disponible';
|
||||
} catch (\Exception $e) {
|
||||
return 'Error: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene la versión del servidor Memcached.
|
||||
*
|
||||
* @return string Versión del servidor Memcached o mensaje de error
|
||||
*/
|
||||
private function getMemcachedVersion(): string
|
||||
{
|
||||
try {
|
||||
$memcached = new Memcached();
|
||||
$memcached->addServer(
|
||||
Config::get('cache.stores.memcached.servers.0.host'),
|
||||
Config::get('cache.stores.memcached.servers.0.port')
|
||||
);
|
||||
|
||||
$stats = $memcached->getStats();
|
||||
foreach ($stats as $serverStats) {
|
||||
return $serverStats['version'] ?? 'No disponible';
|
||||
}
|
||||
|
||||
return 'No disponible';
|
||||
} catch (\Exception $e) {
|
||||
return 'Error: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene la versión del servidor Redis.
|
||||
*
|
||||
* @return string Versión del servidor Redis o mensaje de error
|
||||
*/
|
||||
private function getRedisVersion(): string
|
||||
{
|
||||
try {
|
||||
$info = Redis::info();
|
||||
return $info['redis_version'] ?? 'No disponible';
|
||||
} catch (\Exception $e) {
|
||||
return 'Error: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si un driver específico está en uso en el sistema.
|
||||
*
|
||||
* Comprueba si el driver está siendo utilizado en caché, sesiones o colas.
|
||||
*
|
||||
* @param string $driver Nombre del driver a verificar
|
||||
* @return bool True si el driver está en uso, false en caso contrario
|
||||
*/
|
||||
protected function isDriverInUse(string $driver): bool
|
||||
{
|
||||
return in_array($driver, [
|
||||
Config::get('cache.default'),
|
||||
Config::get('session.driver'),
|
||||
Config::get('queue.default'),
|
||||
]);
|
||||
}
|
||||
}
|
186
src/Application/Cache/KonekoCacheManager copy 2.php
Normal file
186
src/Application/Cache/KonekoCacheManager copy 2.php
Normal file
@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Cache;
|
||||
|
||||
/**
|
||||
* 📊 Gestor de Cache del Ecosistema Koneko
|
||||
* Soporte para múltiples niveles (core, componente, grupo), drivers mixtos y tagging.
|
||||
* Compatible con redis, memcached, file y database.
|
||||
*/
|
||||
class KonekoCacheManager
|
||||
{
|
||||
private string $namespace;
|
||||
private string $component;
|
||||
private string $group;
|
||||
private string $subGroup;
|
||||
|
||||
public function __construct(string $namespace, string $component = 'core')
|
||||
{
|
||||
$this->namespace = $namespace;
|
||||
$this->component = $component;
|
||||
}
|
||||
|
||||
public function setContext(string $component, string $group, string $subGroup): static
|
||||
{
|
||||
return $this
|
||||
->setComponent($component)
|
||||
->setGroup($group)
|
||||
->setSubGroup($subGroup);
|
||||
}
|
||||
|
||||
public function setNamespace(string $namespace): static
|
||||
{
|
||||
$this->validateSlug('namespace', $namespace);
|
||||
|
||||
$this->namespace = strtolower($namespace);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setComponent(string $component): static
|
||||
{
|
||||
$this->validateSlug('component', $component);
|
||||
|
||||
$this->component = strtolower($component);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setGroup(string $group): static
|
||||
{
|
||||
$this->validateSlug('group', $group);
|
||||
|
||||
$this->group = strtolower($group);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setSubGroup(string $subGroup): static
|
||||
{
|
||||
$this->validateSlug('subGroup', $subGroup);
|
||||
|
||||
$this->subGroup = strtolower($subGroup);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
private function ensureContext(): void
|
||||
{
|
||||
foreach (['component', 'group', 'subGroup'] as $context) {
|
||||
if (empty($this->$context)) {
|
||||
throw new \LogicException("Debe establecer {$context} antes de generar una clave de caché.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public function currentNamespace(): string
|
||||
{
|
||||
return $this->namespace;
|
||||
}
|
||||
|
||||
public function currentComponent(): string
|
||||
{
|
||||
return $this->component;
|
||||
}
|
||||
|
||||
public function currentGroup(): string
|
||||
{
|
||||
return $this->group;
|
||||
}
|
||||
|
||||
public function currentSubGroup(): string
|
||||
{
|
||||
return $this->subGroup;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public function fullKey(string $suffix): string
|
||||
{
|
||||
return "{$this->path()}.{$suffix}";
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function key(string $suffix): string
|
||||
{
|
||||
$this->ensureContext();
|
||||
|
||||
return "{$this->path()}.{$suffix}";
|
||||
}
|
||||
|
||||
public function config(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return config($this->key($key), $default);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function ttl(): int
|
||||
{
|
||||
return (int) (
|
||||
config("{$this->namespace}.{$this->component}.{$this->group}.{$this->subGroup}.ttl")
|
||||
?? config("{$this->namespace}.{$this->component}.{$this->group}.ttl")
|
||||
?? config("{$this->namespace}.{$this->component}.cache.ttl")
|
||||
?? config("{$this->namespace}.cache.ttl", 3600)
|
||||
);
|
||||
}
|
||||
|
||||
public function enabled(): bool
|
||||
{
|
||||
return (bool) (
|
||||
config("{$this->namespace}.{$this->component}.{$this->group}.{$this->subGroup}.enabled")
|
||||
?? config("{$this->namespace}.{$this->component}.{$this->group}.enabled")
|
||||
?? config("{$this->namespace}.{$this->component}.cache.enabled")
|
||||
?? config("{$this->namespace}.cache.enabled", true)
|
||||
);
|
||||
}
|
||||
|
||||
public function shouldDebug(): bool
|
||||
{
|
||||
return (bool) $this->config('debug', false);
|
||||
}
|
||||
|
||||
public function driver(): string
|
||||
{
|
||||
return config('cache.default');
|
||||
}
|
||||
|
||||
public function path(): string
|
||||
{
|
||||
return "{$this->namespace}.{$this->component}.{$this->group}.{$this->subGroup}";
|
||||
}
|
||||
|
||||
public function info(): array
|
||||
{
|
||||
return [
|
||||
'namespace' => $this->namespace,
|
||||
'component' => $this->component,
|
||||
'group' => $this->group,
|
||||
'subGroup' => $this->subGroup,
|
||||
'enabled' => $this->enabled(),
|
||||
'ttl' => $this->ttl(),
|
||||
'driver' => $this->driver(),
|
||||
'debug' => $this->shouldDebug(),
|
||||
];
|
||||
}
|
||||
|
||||
private function validateSlug(string $field, string $value): void
|
||||
{
|
||||
if (!preg_match('/^[a-z0-9\-]+$/', $value)) {
|
||||
throw new \InvalidArgumentException("El valor de '{$field}' debe ser un slug válido.");
|
||||
}
|
||||
}
|
||||
|
||||
private function ensureContext(): void
|
||||
{
|
||||
if (empty($this->component) || empty($this->group)) {
|
||||
throw new \LogicException("Debe establecer component y group antes de generar una clave de caché.");
|
||||
}
|
||||
}
|
||||
}
|
185
src/Application/Cache/KonekoCacheManager copy.php
Normal file
185
src/Application/Cache/KonekoCacheManager copy.php
Normal file
@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Cache;
|
||||
|
||||
/**
|
||||
* 📊 Gestor de Cache del Ecosistema Koneko
|
||||
* Soporte para múltiples niveles (core, componente, grupo), drivers mixtos y tagging.
|
||||
* Compatible con redis, memcached, file y database.
|
||||
*/
|
||||
class KonekoCacheManager
|
||||
{
|
||||
private string $namespace;
|
||||
private string $component;
|
||||
private string $group;
|
||||
|
||||
public function __construct(string $namespace)
|
||||
{
|
||||
$this->namespace = $namespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Establece el contexto de la caché.
|
||||
*/
|
||||
public function setContext(string $component, string $group): static
|
||||
{
|
||||
return $this
|
||||
->setNamespace($this->namespace)
|
||||
->setComponent($component)
|
||||
->setGroup($group);
|
||||
}
|
||||
|
||||
/**
|
||||
* Establece el namespace de la caché.
|
||||
*/
|
||||
public function setNamespace(string $namespace): static
|
||||
{
|
||||
if (!preg_match('/^[a-z0-9\-]+$/', $namespace)) {
|
||||
throw new \InvalidArgumentException("El namespace '{$namespace}' debe ser un slug válido.");
|
||||
}
|
||||
|
||||
$this->namespace = strtolower($namespace);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Establece el componente de la caché.
|
||||
*/
|
||||
public function setComponent(string $component): static
|
||||
{
|
||||
if (!preg_match('/^[a-z0-9\-]+$/', $component)) {
|
||||
throw new \InvalidArgumentException("El componente '{$component}' debe ser un slug válido.");
|
||||
}
|
||||
|
||||
$this->component = strtolower($component);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Establece el grupo de la caché.
|
||||
*/
|
||||
public function setGroup(string $group): static
|
||||
{
|
||||
if (!preg_match('/^[a-z0-9\-]+$/', $group)) {
|
||||
throw new \InvalidArgumentException("El grupo '{$group}' debe ser un slug válido.");
|
||||
}
|
||||
|
||||
$this->group = strtolower($group);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function currentNamespace(): string
|
||||
{
|
||||
return $this->namespace;
|
||||
}
|
||||
|
||||
public function currentComponent(): string
|
||||
{
|
||||
return $this->component;
|
||||
}
|
||||
|
||||
public function currentGroup(): string
|
||||
{
|
||||
return $this->group;
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera una clave calificada para la caché.
|
||||
*/
|
||||
public function key(string $suffix): string
|
||||
{
|
||||
if (empty($this->component) || empty($this->group)) {
|
||||
throw new \LogicException("Component and group must be set before generating a cache key.");
|
||||
}
|
||||
|
||||
return "{$this->path()}.{$suffix}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene un valor de configuración.
|
||||
*/
|
||||
public function config(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return config($this->key($key), $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el tiempo de vida (TTL) de la caché.
|
||||
*/
|
||||
public function ttl(): int
|
||||
{
|
||||
return (int) (
|
||||
config("{$this->namespace}.{$this->component}.{$this->group}.ttl") ??
|
||||
config("{$this->namespace}.{$this->component}.cache.ttl") ??
|
||||
config("{$this->namespace}.cache.ttl", 3600)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el estado de habilitación de la caché.
|
||||
*/
|
||||
public function enabled(): bool
|
||||
{
|
||||
return (bool) (
|
||||
config("{$this->namespace}.{$this->component}.{$this->group}.enabled") ??
|
||||
config("{$this->namespace}.{$this->component}.cache.enabled") ??
|
||||
config("{$this->namespace}.cache.enabled", true)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determina si se debe depurar la caché.
|
||||
*/
|
||||
public function shouldDebug(): bool
|
||||
{
|
||||
return (bool) $this->config('debug', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el driver de caché.
|
||||
*/
|
||||
public function driver(): string
|
||||
{
|
||||
return config('cache.default');
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra los valores por defecto en la configuración.
|
||||
*/
|
||||
public function registerDefaults(): void
|
||||
{
|
||||
if (! config()->has($this->key('ttl'))) {
|
||||
config()->set($this->key('ttl'), 3600);
|
||||
}
|
||||
|
||||
if (! config()->has($this->key('enabled'))) {
|
||||
config()->set($this->key('enabled'), true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene la ruta de la caché.
|
||||
*/
|
||||
public function path(): string
|
||||
{
|
||||
return "{$this->namespace}.{$this->component}.{$this->group}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Información extendida de depuración.
|
||||
*/
|
||||
public function info(): array
|
||||
{
|
||||
return [
|
||||
'component' => $this->component,
|
||||
'group' => $this->group,
|
||||
'enabled' => $this->enabled(),
|
||||
'ttl' => $this->ttl(),
|
||||
'driver' => $this->driver(),
|
||||
'debug' => $this->shouldDebug(),
|
||||
];
|
||||
}
|
||||
}
|
232
src/Application/Cache/KonekoCacheManager.php
Normal file
232
src/Application/Cache/KonekoCacheManager.php
Normal file
@ -0,0 +1,232 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Cache;
|
||||
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Support\Facades\{Auth, Config};
|
||||
use Illuminate\Support\Str;
|
||||
use Koneko\VuexyAdmin\Application\Enums\Settings\SettingScope;
|
||||
use Koneko\VuexyAdmin\Models\User;
|
||||
|
||||
class KonekoCacheManager
|
||||
{
|
||||
public const MAX_KEY_LENGTH = 120; // Friendly limit
|
||||
private const DEFAULT_SCOPE = SettingScope::GLOBAL->value;
|
||||
private const USER_GUEST_ALIAS = SettingScope::GUEST->value;
|
||||
|
||||
private string $namespace;
|
||||
private string $scope;
|
||||
|
||||
private string $component;
|
||||
private string $group;
|
||||
private string $subGroup;
|
||||
|
||||
protected bool $isUserScoped = false;
|
||||
protected ?Authenticatable $user = null;
|
||||
|
||||
public function __construct(string $namespace)
|
||||
{
|
||||
if (empty($namespace) || !preg_match('/^[a-z0-9\-]+$/', $namespace)) {
|
||||
throw new \InvalidArgumentException("El namespace '{$namespace}' no es válido.");
|
||||
}
|
||||
|
||||
$this->namespace = $this->truncate('namespace', $namespace, 8);
|
||||
$this->scope = self::DEFAULT_SCOPE;
|
||||
}
|
||||
|
||||
|
||||
// ========= CONTEXT MANAGEMENT =========
|
||||
|
||||
public function setContext(
|
||||
string $component,
|
||||
string $group,
|
||||
string $subGroup,
|
||||
string $scope = self::DEFAULT_SCOPE
|
||||
): static {
|
||||
return $this
|
||||
->setComponent($component)
|
||||
->setGroup($group)
|
||||
->setSubGroup($subGroup)
|
||||
->setScope($scope);
|
||||
}
|
||||
|
||||
public function setScope(SettingScope|string $scope): static
|
||||
{
|
||||
if (is_string($scope) && !SettingScope::isValid($scope)) {
|
||||
throw new \InvalidArgumentException("Scope '{$scope}' no es válido.");
|
||||
}
|
||||
|
||||
$this->scope = is_string($scope)
|
||||
? SettingScope::from($scope)->value
|
||||
: $scope->value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function setNamespace(string $namespace): static
|
||||
{
|
||||
$this->namespace = $this->truncate('namespace', $namespace, 8);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setComponent(string $component): static
|
||||
{
|
||||
$this->component = $this->truncate('component', $component, 16);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setGroup(string $group): static
|
||||
{
|
||||
$this->group = $this->truncate('group', $group, 16);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setSubGroup(string $subGroup): static
|
||||
{
|
||||
$this->subGroup = $this->truncate('subGroup', $subGroup, 16);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setUser(int|Authenticatable|null|false $user = null): static
|
||||
{
|
||||
match (true) {
|
||||
$user === false => $this->resetUserScope(), // Visitante explícito
|
||||
is_int($user) => $this->assignUser(User::findOrFail($user)),
|
||||
$user instanceof Authenticatable => $this->assignUser($user),
|
||||
default => $this->assignUser(Auth::user()) // Usuario autenticado o null (visitante)
|
||||
};
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function assignUser(?Authenticatable $user): void
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->isUserScoped = !is_null($user);
|
||||
}
|
||||
|
||||
protected function resetUserScope(): void
|
||||
{
|
||||
$this->user = null;
|
||||
$this->isUserScoped = false;
|
||||
}
|
||||
|
||||
public function isUserScoped(): bool
|
||||
{
|
||||
return $this->isUserScoped;
|
||||
}
|
||||
|
||||
// ========= ACCESSORS =========
|
||||
|
||||
public function currentNamespace(): string { return $this->namespace; }
|
||||
public function currentComponent(): string { return $this->component; }
|
||||
public function currentGroup(): string { return $this->group; }
|
||||
public function currentSubGroup(): string { return $this->subGroup; }
|
||||
public function currentScope(): string { return $this->scope; }
|
||||
|
||||
// ========= CACHE CONFIGURATION =========
|
||||
|
||||
public function key(string $suffix): string
|
||||
{
|
||||
$this->ensureContext();
|
||||
|
||||
$userSegment = $this->isUserScoped
|
||||
? 'u.' . ($this->user?->getAuthIdentifier() ?? self::USER_GUEST_ALIAS)
|
||||
: self::DEFAULT_SCOPE;
|
||||
|
||||
$scopeSegment = $this->scope === self::DEFAULT_SCOPE
|
||||
? null
|
||||
: "scope." . $this->scope;
|
||||
|
||||
$base = implode('.', array_filter([
|
||||
$this->namespace,
|
||||
app()->environment(),
|
||||
$this->component,
|
||||
$this->group,
|
||||
$this->subGroup,
|
||||
$scopeSegment,
|
||||
$userSegment,
|
||||
$suffix
|
||||
]));
|
||||
|
||||
return strlen($base) > self::MAX_KEY_LENGTH
|
||||
? 'h:' . crc32($base)
|
||||
: $base;
|
||||
}
|
||||
|
||||
public function ttl(): int
|
||||
{
|
||||
return (int) (
|
||||
Config::get("{$this->path()}.ttl") ??
|
||||
Config::get("{$this->namespace}.{$this->component}.{$this->group}.ttl") ??
|
||||
Config::get("{$this->namespace}.{$this->component}.cache.ttl") ??
|
||||
Config::get("{$this->namespace}.cache.ttl", 3600)
|
||||
);
|
||||
}
|
||||
|
||||
public function enabled(): bool
|
||||
{
|
||||
return (bool) (
|
||||
Config::get("{$this->path()}.enabled") ??
|
||||
Config::get("{$this->namespace}.{$this->component}.{$this->group}.enabled") ??
|
||||
Config::get("{$this->namespace}.{$this->component}.cache.enabled") ??
|
||||
Config::get("{$this->namespace}.cache.enabled", true)
|
||||
);
|
||||
}
|
||||
|
||||
public function driver(): string
|
||||
{
|
||||
return Config::get('cache.default');
|
||||
}
|
||||
|
||||
public function path(): string
|
||||
{
|
||||
return "{$this->namespace}." . app()->environment() . ".{$this->component}.{$this->group}.{$this->subGroup}";
|
||||
}
|
||||
|
||||
public function info(): array
|
||||
{
|
||||
return [
|
||||
'environment' => app()->environment(),
|
||||
'namespace' => $this->namespace,
|
||||
'component' => $this->component,
|
||||
'group' => $this->group,
|
||||
'subGroup' => $this->subGroup,
|
||||
'scope' => $this->scope,
|
||||
'enabled' => $this->enabled(),
|
||||
'ttl' => $this->ttl(),
|
||||
'driver' => $this->driver(),
|
||||
];
|
||||
}
|
||||
|
||||
// ========= VALIDATION & SANITIZATION =========
|
||||
|
||||
private function validateSlug(string $field, string $value): void
|
||||
{
|
||||
if (!preg_match('/^[a-z0-9\-]+$/', $value)) {
|
||||
throw new \InvalidArgumentException("El valor de '{$field}' debe ser un slug válido.");
|
||||
}
|
||||
}
|
||||
|
||||
private function ensureContext(): void
|
||||
{
|
||||
foreach (['namespace', 'component', 'group', 'subGroup'] as $prop) {
|
||||
if (empty($this->$prop) || !preg_match('/^[a-z0-9\-]+$/', $this->$prop)) {
|
||||
throw new \InvalidArgumentException("El valor de '{$prop}' es obligatorio y debe ser un slug válido.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private function truncate(string $field, string $value, int $maxLength): string
|
||||
{
|
||||
$this->validateSlug($field, $value);
|
||||
|
||||
return Str::limit(strtolower($value), $maxLength, '');
|
||||
}
|
||||
}
|
155
src/Application/Cache/KonekoSessionManager.php
Normal file
155
src/Application/Cache/KonekoSessionManager.php
Normal file
@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Cache;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class KonekoSessionManager
|
||||
{
|
||||
private string $driver;
|
||||
|
||||
public function __construct(mixed $driver = null)
|
||||
{
|
||||
$this->driver = $driver ?? config('session.driver');
|
||||
}
|
||||
|
||||
public function getSessionStats(mixed $driver = null): array
|
||||
{
|
||||
$driver = $driver ?? $this->driver;
|
||||
|
||||
if (!$this->isSupportedDriver($driver))
|
||||
return $this->response('warning', 'Driver no soportado o no configurado.', ['session_count' => 0]);
|
||||
|
||||
try {
|
||||
switch ($driver) {
|
||||
case 'redis':
|
||||
return $this->getRedisStats();
|
||||
|
||||
case 'database':
|
||||
return $this->getDatabaseStats();
|
||||
|
||||
case 'file':
|
||||
return $this->getFileStats();
|
||||
|
||||
default:
|
||||
return $this->response('warning', 'Driver no reconocido.', ['session_count' => 0]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return $this->response('danger', 'Error al obtener estadísticas: ' . $e->getMessage(), ['session_count' => 0]);
|
||||
}
|
||||
}
|
||||
|
||||
public function clearSessions(mixed $driver = null): array
|
||||
{
|
||||
$driver = $driver ?? $this->driver;
|
||||
|
||||
if (!$this->isSupportedDriver($driver)) {
|
||||
return $this->response('warning', 'Driver no soportado o no configurado.');
|
||||
}
|
||||
|
||||
try {
|
||||
switch ($driver) {
|
||||
case 'redis':
|
||||
return $this->clearRedisSessions();
|
||||
|
||||
case 'memcached':
|
||||
Cache::getStore()->flush();
|
||||
return $this->response('success', 'Se eliminó la memoria caché de sesiones en Memcached.');
|
||||
|
||||
case 'database':
|
||||
DB::table('sessions')->truncate();
|
||||
return $this->response('success', 'Se eliminó la memoria caché de sesiones en la base de datos.');
|
||||
|
||||
case 'file':
|
||||
return $this->clearFileSessions();
|
||||
|
||||
default:
|
||||
return $this->response('warning', 'Driver no reconocido.');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return $this->response('danger', 'Error al limpiar las sesiones: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function getRedisStats()
|
||||
{
|
||||
$prefix = config('cache.prefix'); // Asegúrate de agregar el sufijo correcto si es necesario
|
||||
$keys = Redis::connection('sessions')->keys($prefix . '*');
|
||||
|
||||
return $this->response('success', 'Se ha recargado la información de la caché de Redis.', ['session_count' => count($keys)]);
|
||||
}
|
||||
|
||||
private function getDatabaseStats(): array
|
||||
{
|
||||
$sessionCount = DB::table('sessions')->count();
|
||||
|
||||
return $this->response('success', 'Se ha recargado la información de la base de datos.', ['session_count' => $sessionCount]);
|
||||
}
|
||||
|
||||
private function getFileStats(): array
|
||||
{
|
||||
$cachePath = config('session.files');
|
||||
$files = glob($cachePath . '/*');
|
||||
|
||||
return $this->response('success', 'Se ha recargado la información de sesiones de archivos.', ['session_count' => count($files)]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Limpia sesiones en Redis.
|
||||
*/
|
||||
private function clearRedisSessions(): array
|
||||
{
|
||||
$prefix = config('cache.prefix', '');
|
||||
$keys = Redis::connection('sessions')->keys($prefix . '*');
|
||||
|
||||
if (!empty($keys)) {
|
||||
Redis::connection('sessions')->flushdb();
|
||||
|
||||
// Simulate cache clearing delay
|
||||
sleep(1);
|
||||
|
||||
return $this->response('success', 'Se eliminó la memoria caché de sesiones en Redis.');
|
||||
}
|
||||
|
||||
return $this->response('info', 'No se encontraron claves para eliminar en Redis.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpia sesiones en archivos.
|
||||
*/
|
||||
private function clearFileSessions(): array
|
||||
{
|
||||
$cachePath = config('session.files');
|
||||
$files = glob($cachePath . '/*');
|
||||
|
||||
if (!empty($files)) {
|
||||
foreach ($files as $file) {
|
||||
unlink($file);
|
||||
}
|
||||
|
||||
return $this->response('success', 'Se eliminó la memoria caché de sesiones en archivos.');
|
||||
}
|
||||
|
||||
return $this->response('info', 'No se encontraron sesiones en archivos para eliminar.');
|
||||
}
|
||||
|
||||
|
||||
private function isSupportedDriver(string $driver): bool
|
||||
{
|
||||
return in_array($driver, ['redis', 'memcached', 'database', 'file']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera una respuesta estandarizada.
|
||||
*/
|
||||
private function response(string $status, string $message, array $data = []): array
|
||||
{
|
||||
return array_merge(compact('status', 'message'), $data);
|
||||
}
|
||||
}
|
479
src/Application/Cache/LaravelCacheManager.php
Normal file
479
src/Application/Cache/LaravelCacheManager.php
Normal file
@ -0,0 +1,479 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Cache;
|
||||
|
||||
use Illuminate\Support\Facades\{Cache,DB,Redis,File};
|
||||
use Memcached;
|
||||
|
||||
/**
|
||||
* Servicio para gestionar y administrar el sistema de caché.
|
||||
*
|
||||
* Esta clase proporciona funcionalidades para administrar diferentes drivers de caché
|
||||
* (Redis, Memcached, Database, File), incluyendo operaciones como obtener estadísticas,
|
||||
* limpiar la caché y monitorear el uso de recursos.
|
||||
*/
|
||||
class LaravelCacheManager
|
||||
{
|
||||
/** @var string Driver de caché actualmente seleccionado */
|
||||
private string $driver;
|
||||
|
||||
/**
|
||||
* Constructor del servicio de gestión de caché.
|
||||
*
|
||||
* @param mixed $driver Driver de caché a utilizar. Si es null, se usa el driver predeterminado
|
||||
*/
|
||||
public function __construct(mixed $driver = null)
|
||||
{
|
||||
$this->driver = $driver ?? config('cache.default');
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene estadísticas de caché para el driver especificado.
|
||||
*
|
||||
* Recopila información detallada sobre el uso y rendimiento del sistema de caché,
|
||||
* incluyendo uso de memoria, número de elementos y estadísticas específicas del driver.
|
||||
*
|
||||
* @param mixed $driver Driver de caché del cual obtener estadísticas
|
||||
* @return array Estadísticas del sistema de caché
|
||||
*/
|
||||
public function getCacheStats(mixed $driver = null): array
|
||||
{
|
||||
$driver = $driver ?? $this->driver;
|
||||
|
||||
if (!$this->isSupportedDriver($driver)) {
|
||||
return $this->response('warning', 'Driver no soportado o no configurado.');
|
||||
}
|
||||
|
||||
try {
|
||||
return match ($driver) {
|
||||
'database' => $this->_getDatabaseStats(),
|
||||
'file' => $this->_getFilecacheStats(),
|
||||
'redis' => $this->_getRedisStats(),
|
||||
'memcached' => $this->_getMemcachedStats(),
|
||||
default => $this->response('info', 'No hay estadísticas disponibles para este driver.'),
|
||||
};
|
||||
} catch (\Exception $e) {
|
||||
return $this->response('danger', 'Error al obtener estadísticas: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpia la caché del driver especificado.
|
||||
*
|
||||
* @param mixed $driver Driver de caché a limpiar
|
||||
* @return array Resultado de la operación de limpieza
|
||||
*/
|
||||
public function clearCache(mixed $driver = null): array
|
||||
{
|
||||
$driver = $driver ?? $this->driver;
|
||||
|
||||
if (!$this->isSupportedDriver($driver)) {
|
||||
return $this->response('warning', 'Driver no soportado o no configurado.');
|
||||
}
|
||||
|
||||
try {
|
||||
switch ($driver) {
|
||||
case 'redis':
|
||||
$keysCleared = $this->clearRedisCache();
|
||||
|
||||
return $keysCleared
|
||||
? $this->response('warning', 'Se ha purgado toda la caché de Redis.')
|
||||
: $this->response('info', 'No se encontraron claves en Redis para eliminar.');
|
||||
|
||||
case 'memcached':
|
||||
$keysCleared = $this->clearMemcachedCache();
|
||||
|
||||
return $keysCleared
|
||||
? $this->response('warning', 'Se ha purgado toda la caché de Memcached.')
|
||||
: $this->response('info', 'No se encontraron claves en Memcached para eliminar.');
|
||||
|
||||
case 'database':
|
||||
$rowsDeleted = $this->clearDatabaseCache();
|
||||
|
||||
return $rowsDeleted
|
||||
? $this->response('warning', 'Se ha purgado toda la caché almacenada en la base de datos.')
|
||||
: $this->response('info', 'No se encontraron registros en la caché de la base de datos.');
|
||||
|
||||
case 'file':
|
||||
$filesDeleted = $this->clearFilecache();
|
||||
|
||||
return $filesDeleted
|
||||
? $this->response('warning', 'Se ha purgado toda la caché de archivos.')
|
||||
: $this->response('info', 'No se encontraron archivos en la caché para eliminar.');
|
||||
|
||||
default:
|
||||
Cache::flush();
|
||||
|
||||
return $this->response('warning', 'Caché purgada.');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return $this->response('danger', 'Error al limpiar la caché: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene estadísticas detalladas del servidor Redis.
|
||||
*
|
||||
* @return array Información detallada del servidor Redis incluyendo versión, memoria, clientes y más
|
||||
*/
|
||||
public function getRedisStats()
|
||||
{
|
||||
try {
|
||||
if (!Redis::ping()) {
|
||||
return $this->response('warning', 'No se puede conectar con el servidor Redis.');
|
||||
}
|
||||
|
||||
$info = Redis::info();
|
||||
|
||||
$databases = $this->getRedisDatabases();
|
||||
|
||||
$redisInfo = [
|
||||
'server' => config('database.redis.default.host'),
|
||||
'redis_version' => $info['redis_version'] ?? 'N/A',
|
||||
'os' => $info['os'] ?? 'N/A',
|
||||
'tcp_port' => $info['tcp_port'] ?? 'N/A',
|
||||
'connected_clients' => $info['connected_clients'] ?? 'N/A',
|
||||
'blocked_clients' => $info['blocked_clients'] ?? 'N/A',
|
||||
'maxmemory' => $info['maxmemory'] ?? 0,
|
||||
'used_memory_human' => $info['used_memory_human'] ?? 'N/A',
|
||||
'used_memory_peak' => $info['used_memory_peak'] ?? 'N/A',
|
||||
'used_memory_peak_human' => $info['used_memory_peak_human'] ?? 'N/A',
|
||||
'total_system_memory' => $info['total_system_memory'] ?? 0,
|
||||
'total_system_memory_human' => $info['total_system_memory_human'] ?? 'N/A',
|
||||
'maxmemory_human' => $info['maxmemory_human'] !== '0B' ? $info['maxmemory_human'] : 'Sin Límite',
|
||||
'total_connections_received' => number_format($info['total_connections_received']) ?? 'N/A',
|
||||
'total_commands_processed' => number_format($info['total_commands_processed']) ?? 'N/A',
|
||||
'maxmemory_policy' => $info['maxmemory_policy'] ?? 'N/A',
|
||||
'role' => $info['role'] ?? 'N/A',
|
||||
'cache_database' => '',
|
||||
'sessions_database' => '',
|
||||
'general_database' => ',',
|
||||
'keys' => $databases['total_keys'],
|
||||
'used_memory' => $info['used_memory'] ?? 0,
|
||||
'uptime' => gmdate('H\h i\m s\s', $info['uptime_in_seconds'] ?? 0),
|
||||
'databases' => $databases,
|
||||
];
|
||||
|
||||
return $this->response('success', 'Se a recargado las estadísticas de Redis.', ['info' => $redisInfo]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->response('danger', 'Error al conectar con el servidor Redis: ' . Redis::getLastError());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene estadísticas detalladas del servidor Memcached.
|
||||
*
|
||||
* @return array Información detallada del servidor Memcached incluyendo versión, memoria y estadísticas de uso
|
||||
*/
|
||||
public function getMemcachedStats()
|
||||
{
|
||||
try {
|
||||
$memcachedStats = [];
|
||||
|
||||
// Crear instancia del cliente Memcached
|
||||
$memcached = new Memcached();
|
||||
$memcached->addServer(config('memcached.host'), config('memcached.port'));
|
||||
|
||||
// Obtener estadísticas del servidor
|
||||
$stats = $memcached->getStats();
|
||||
|
||||
foreach ($stats as $server => $data) {
|
||||
$server = explode(':', $server);
|
||||
|
||||
$memcachedStats[] = [
|
||||
'server' => $server[0],
|
||||
'tcp_port' => $server[1],
|
||||
'uptime' => $data['uptime'] ?? 'N/A',
|
||||
'version' => $data['version'] ?? 'N/A',
|
||||
'libevent' => $data['libevent'] ?? 'N/A',
|
||||
'max_connections' => $data['max_connections'] ?? 0,
|
||||
'total_connections' => $data['total_connections'] ?? 0,
|
||||
'rejected_connections' => $data['rejected_connections'] ?? 0,
|
||||
'curr_items' => $data['curr_items'] ?? 0, // Claves almacenadas
|
||||
'bytes' => $data['bytes'] ?? 0, // Memoria usada
|
||||
'limit_maxbytes' => $data['limit_maxbytes'] ?? 0, // Memoria máxima
|
||||
'cmd_get' => $data['cmd_get'] ?? 0, // Comandos GET ejecutados
|
||||
'cmd_set' => $data['cmd_set'] ?? 0, // Comandos SET ejecutados
|
||||
'get_hits' => $data['get_hits'] ?? 0, // GET exitosos
|
||||
'get_misses' => $data['get_misses'] ?? 0, // GET fallidos
|
||||
'evictions' => $data['evictions'] ?? 0, // Claves expulsadas
|
||||
'bytes_read' => $data['bytes_read'] ?? 0, // Bytes leídos
|
||||
'bytes_written' => $data['bytes_written'] ?? 0, // Bytes escritos
|
||||
'total_items' => $data['total_items'] ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
return $this->response('success', 'Se a recargado las estadísticas de Memcached.', ['info' => $memcachedStats]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->response('danger', 'Error al conectar con el servidor Memcached: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene estadísticas para caché en base de datos.
|
||||
*
|
||||
* @return array Estadísticas de la caché en base de datos incluyendo cantidad de registros y uso de memoria
|
||||
*/
|
||||
private function _getDatabaseStats(): array
|
||||
{
|
||||
try {
|
||||
$recordCount = DB::table('cache')->count();
|
||||
$tableInfo = DB::select("SHOW TABLE STATUS WHERE Name = 'cache'");
|
||||
|
||||
$memory_usage = isset($tableInfo[0]) ? $this->formatBytes($tableInfo[0]->Data_length + $tableInfo[0]->Index_length) : 'N/A';
|
||||
|
||||
return $this->response('success', 'Se ha recargado la información de la caché de base de datos.', ['item_count' => $recordCount, 'memory_usage' => $memory_usage]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->response('danger', 'Error al obtener estadísticas de la base de datos: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene estadísticas para caché en archivos.
|
||||
*
|
||||
* @return array Estadísticas de la caché en archivos incluyendo cantidad de archivos y uso de memoria
|
||||
*/
|
||||
private function _getFilecacheStats(): array
|
||||
{
|
||||
try {
|
||||
$cachePath = config('cache.stores.file.path');
|
||||
$files = glob($cachePath . '/*');
|
||||
|
||||
$memory_usage = $this->formatBytes(array_sum(array_map('filesize', $files)));
|
||||
|
||||
return $this->response('success', 'Se ha recargado la información de la caché de archivos.', ['item_count' => count($files), 'memory_usage' => $memory_usage]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->response('danger', 'Error al obtener estadísticas de archivos: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene estadísticas específicas de Redis para la caché.
|
||||
*
|
||||
* @return array Estadísticas de Redis incluyendo cantidad de claves y uso de memoria
|
||||
*/
|
||||
private function _getRedisStats()
|
||||
{
|
||||
try {
|
||||
$prefix = config('cache.prefix'); // Asegúrate de agregar el sufijo correcto si es necesario
|
||||
|
||||
$info = Redis::info();
|
||||
$keys = Redis::connection('cache')->keys($prefix . '*');
|
||||
|
||||
$memory_usage = $this->formatBytes($info['used_memory'] ?? 0);
|
||||
|
||||
return $this->response('success', 'Se ha recargado la información de la caché de Redis.', ['item_count' => count($keys), 'memory_usage' => $memory_usage]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->response('danger', 'Error al obtener estadísticas de Redis: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene estadísticas específicas de Memcached para la caché.
|
||||
*
|
||||
* @return array Estadísticas de Memcached incluyendo cantidad de elementos y uso de memoria
|
||||
*/
|
||||
public function _getMemcachedStats(): array
|
||||
{
|
||||
try {
|
||||
// Obtener estadísticas generales del servidor
|
||||
$stats = Cache::getStore()->getMemcached()->getStats();
|
||||
|
||||
if (empty($stats)) {
|
||||
return $this->response('error', 'No se pudieron obtener las estadísticas del servidor Memcached.', ['item_count' => 0, 'memory_usage' => 0]);
|
||||
}
|
||||
|
||||
// Usar el primer servidor configurado (en la mayoría de los casos hay uno)
|
||||
$serverStats = array_shift($stats);
|
||||
|
||||
return $this->response(
|
||||
'success',
|
||||
'Estadísticas del servidor Memcached obtenidas correctamente.',
|
||||
[
|
||||
'item_count' => $serverStats['curr_items'] ?? 0, // Número total de claves
|
||||
'memory_usage' => $this->formatBytes($serverStats['bytes'] ?? 0), // Memoria usada
|
||||
'max_memory' => $this->formatBytes($serverStats['limit_maxbytes'] ?? 0), // Memoria máxima asignada
|
||||
]
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
return $this->response('danger', 'Error al obtener estadísticas de Memcached: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene información sobre las bases de datos Redis en uso.
|
||||
*
|
||||
* Analiza y recopila información sobre las diferentes bases de datos Redis
|
||||
* configuradas en el sistema (default, cache, sessions).
|
||||
*
|
||||
* @return array Información detallada de las bases de datos Redis
|
||||
*/
|
||||
private function getRedisDatabases(): array
|
||||
{
|
||||
// Verificar si Redis está en uso
|
||||
$isRedisUsed = collect([
|
||||
config('cache.default'),
|
||||
config('session.driver'),
|
||||
config('queue.default'),
|
||||
])->contains('redis');
|
||||
|
||||
if (!$isRedisUsed) {
|
||||
return []; // Si Redis no está en uso, devolver un arreglo vacío
|
||||
}
|
||||
|
||||
// Configuraciones de bases de datos de Redis según su uso
|
||||
$databases = [
|
||||
'default' => config('database.redis.default.database', 0), // REDIS_DB
|
||||
'cache' => config('database.redis.cache.database', 0), // REDIS_CACHE_DB
|
||||
'sessions' => config('database.redis.sessions.database', 0), // REDIS_SESSION_DB
|
||||
];
|
||||
|
||||
$result = [];
|
||||
$totalKeys = 0;
|
||||
|
||||
// Recorrer solo las bases configuradas y activas
|
||||
foreach ($databases as $type => $db) {
|
||||
Redis::select($db); // Seleccionar la base de datos
|
||||
|
||||
$keys = Redis::dbsize(); // Contar las claves en la base
|
||||
|
||||
if ($keys > 0) {
|
||||
$result[$type] = [
|
||||
'database' => $db,
|
||||
'keys' => $keys,
|
||||
];
|
||||
|
||||
$totalKeys += $keys;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($result)) {
|
||||
$result['total_keys'] = $totalKeys;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpia la caché almacenada en base de datos.
|
||||
*
|
||||
* @return bool True si se eliminaron registros, False si no había registros para eliminar
|
||||
*/
|
||||
private function clearDatabaseCache(): bool
|
||||
{
|
||||
$count = DB::table(config('cache.stores.database.table'))->count();
|
||||
|
||||
if ($count > 0) {
|
||||
DB::table(config('cache.stores.database.table'))->truncate();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpia la caché almacenada en archivos.
|
||||
*
|
||||
* @return bool True si se eliminaron archivos, False si no había archivos para eliminar
|
||||
*/
|
||||
private function clearFilecache(): bool
|
||||
{
|
||||
$cachePath = config('cache.stores.file.path');
|
||||
$files = glob($cachePath . '/*');
|
||||
|
||||
if (!empty($files)) {
|
||||
File::deleteDirectory($cachePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpia la caché almacenada en Redis.
|
||||
*
|
||||
* @return bool True si se eliminaron claves, False si no había claves para eliminar
|
||||
*/
|
||||
private function clearRedisCache(): bool
|
||||
{
|
||||
$prefix = config('cache.prefix', '');
|
||||
$keys = Redis::connection('cache')->keys($prefix . '*');
|
||||
|
||||
if (!empty($keys)) {
|
||||
Redis::connection('cache')->flushdb();
|
||||
|
||||
// Simulate cache clearing delay
|
||||
sleep(1);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpia la caché almacenada en Memcached.
|
||||
*
|
||||
* @return bool True si se limpió la caché, False en caso contrario
|
||||
*/
|
||||
private function clearMemcachedCache(): bool
|
||||
{
|
||||
// Obtener el cliente Memcached directamente
|
||||
$memcached = Cache::store('memcached')->getStore()->getMemcached();
|
||||
|
||||
// Ejecutar flush para eliminar todo
|
||||
if ($memcached->flush()) {
|
||||
// Simulate cache clearing delay
|
||||
sleep(1);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si un driver es soportado por el sistema.
|
||||
*
|
||||
* @param string $driver Nombre del driver a verificar
|
||||
* @return bool True si el driver es soportado, False en caso contrario
|
||||
*/
|
||||
private function isSupportedDriver(string $driver): bool
|
||||
{
|
||||
return in_array($driver, ['redis', 'memcached', 'database', 'file']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convierte bytes en un formato legible por humanos.
|
||||
*
|
||||
* @param int|float $bytes Cantidad de bytes a formatear
|
||||
* @return string Cantidad formateada con unidad (B, KB, MB, GB, TB)
|
||||
*/
|
||||
private function formatBytes(int|float $bytes): string
|
||||
{
|
||||
if ($bytes < 1) {
|
||||
return '0 B';
|
||||
}
|
||||
|
||||
$sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
$factor = floor(log($bytes, 1024)); // Más preciso y directo
|
||||
$formatted = $bytes / pow(1024, $factor);
|
||||
|
||||
return sprintf('%.2f', $formatted) . ' ' . $sizes[$factor];
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera una respuesta estandarizada para las operaciones del servicio.
|
||||
*
|
||||
* @param string $status Estado de la operación ('success', 'warning', 'danger', 'info')
|
||||
* @param string $message Mensaje descriptivo de la operación
|
||||
* @param array $data Datos adicionales de la operación
|
||||
* @return array Respuesta estructurada con estado, mensaje y datos
|
||||
*/
|
||||
private function response(string $status, string $message, array $data = []): array
|
||||
{
|
||||
return array_merge(compact('status', 'message'), $data);
|
||||
}
|
||||
}
|
191
src/Application/Cache/VuexyVarsBuilderService.php
Normal file
191
src/Application/Cache/VuexyVarsBuilderService.php
Normal file
@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Cache;
|
||||
|
||||
use Illuminate\Support\Facades\{Cache,Config,Schema};
|
||||
use Koneko\VuexyAdmin\Support\Cache\AbstractKeyValueCacheBuilder;
|
||||
|
||||
/**
|
||||
* 🎛️ Servicio de gestión de variables Vuexy Admin y Customizer.
|
||||
*/
|
||||
class VuexyVarsBuilderService extends AbstractKeyValueCacheBuilder
|
||||
{
|
||||
// Namespace base
|
||||
private const COMPONENT = 'core';
|
||||
private const GROUP = 'layout';
|
||||
|
||||
//protected string $group = 'layout-builder';
|
||||
|
||||
// Cache scope
|
||||
protected bool $isUserScoped = true;
|
||||
|
||||
/** @var string Settings & Cache key */
|
||||
private const SETTINGS_ADMIN_VARS_KEY = 'admin-vars';
|
||||
public const SETTINGS_CUSTOMIZER_VARS_KEY = 'customizer-vars';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(self::COMPONENT, self::GROUP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene las variables administrativas principales.
|
||||
*/
|
||||
public function getAdminVars(?string $key = null): mixed
|
||||
{
|
||||
/*
|
||||
if (!Schema::hasTable('settings')) {
|
||||
return $this->getDefaultAdminVars($key);
|
||||
}
|
||||
*/
|
||||
|
||||
$vars = $this->rememberCache(self::SETTINGS_ADMIN_VARS_KEY, function () {
|
||||
$settings = settings()->setContext($this->component, $this->group)->getGroup(self::SETTINGS_ADMIN_VARS_KEY);
|
||||
|
||||
return $this->buildAdminVarsArray($settings);
|
||||
});
|
||||
|
||||
return $key ? ($vars[$key] ?? null) : $vars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene las configuraciones del customizador Vuexy.
|
||||
*/
|
||||
public function getVuexyCustomizerVars(): array
|
||||
{
|
||||
/*
|
||||
if (!Schema::hasTable('settings')) {
|
||||
return $this->getDefaultVuexyVars();
|
||||
}
|
||||
*/
|
||||
|
||||
return $this->rememberCache(self::SETTINGS_CUSTOMIZER_VARS_KEY, function () {
|
||||
$settings = settings()->setContext($this->component, $this->group)->getGroup(self::SETTINGS_CUSTOMIZER_VARS_KEY);
|
||||
|
||||
return $this->buildVuexyCustomizerVars($settings);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Elimina las configuraciones del customizador Vuexy.
|
||||
*/
|
||||
public static function deleteVuexyCustomizerVars(): void
|
||||
{
|
||||
$instance = new static();
|
||||
|
||||
$instance->setContext($instance->component, $instance->group);
|
||||
|
||||
settings()->setContext($instance->component, $instance->group);
|
||||
settings()->deleteGroup(self::SETTINGS_CUSTOMIZER_VARS_KEY);
|
||||
|
||||
Cache::forget($instance->generateCacheKey(self::SETTINGS_CUSTOMIZER_VARS_KEY));
|
||||
Cache::forget(cache_manager($instance->component, $instance->group)->key(self::SETTINGS_CUSTOMIZER_VARS_KEY));
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpia las caches del admin y del customizador Vuexy.
|
||||
*/
|
||||
public static function clearCache(): void
|
||||
{
|
||||
//Cache::forget(self::SETTINGS_ADMIN_VARS_KEY);
|
||||
//Cache::forget(self::SETTINGS_CUSTOMIZER_VARS_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construye las variables del admin.
|
||||
*/
|
||||
private function buildAdminVarsArray(array $settings): array
|
||||
{
|
||||
return [
|
||||
'title' => $settings['title'] ?? config("{$this->namespace}.title", 'Default Title'),
|
||||
'author' => $settings['author'] ?? config("{$this->namespace}.author", 'Default Author'),
|
||||
'description' => $settings['description'] ?? config("{$this->namespace}.description", 'Default Description'),
|
||||
'favicon' => $this->buildFaviconPaths($settings),
|
||||
'app_name' => $settings['app_name'] ?? config("{$this->namespace}.app_name", 'Default App Name'),
|
||||
'image_logo' => $this->buildImageLogoPaths($settings),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Construye las variables de Vuexy customizer.
|
||||
*/
|
||||
private function buildVuexyCustomizerVars(array $settings): array
|
||||
{
|
||||
$defaults = config("{$this->namespace}.admin.vuexy");
|
||||
|
||||
return collect($defaults)
|
||||
->mapWithKeys(function ($defaultValue, $key) use ($settings) {
|
||||
$vuexyKey = $key;
|
||||
$value = $settings[$vuexyKey] ?? $defaultValue;
|
||||
|
||||
if (in_array($key, [
|
||||
'hasCustomizer', 'displayCustomizer', 'footerFixed',
|
||||
'menuFixed', 'menuCollapsed', 'showDropdownOnHover'
|
||||
], true)) {
|
||||
$value = filter_var($value, FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
|
||||
return [$key => $value];
|
||||
})
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Construye las rutas de favicon.
|
||||
*/
|
||||
private function buildFaviconPaths(array $settings): array
|
||||
{
|
||||
$namespace = $settings['favicon_ns'] ?? null;
|
||||
$defaultFavicon = config("{$this->namespace}.favicon", 'favicon.ico');
|
||||
|
||||
return [
|
||||
'namespace' => $namespace,
|
||||
'16x16' => $namespace ? "{$namespace}_16x16.png" : $defaultFavicon,
|
||||
'76x76' => $namespace ? "{$namespace}_76x76.png" : $defaultFavicon,
|
||||
'120x120' => $namespace ? "{$namespace}_120x120.png" : $defaultFavicon,
|
||||
'152x152' => $namespace ? "{$namespace}_152x152.png" : $defaultFavicon,
|
||||
'180x180' => $namespace ? "{$namespace}_180x180.png" : $defaultFavicon,
|
||||
'192x192' => $namespace ? "{$namespace}_192x192.png" : $defaultFavicon,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Construye las rutas de logos.
|
||||
*/
|
||||
private function buildImageLogoPaths(array $settings): array
|
||||
{
|
||||
$defaultLogo = config("{$this->namespace}.app_logo", 'logo-default.png');
|
||||
|
||||
return [
|
||||
'small' => $settings['image_logo_small'] ?? $defaultLogo,
|
||||
'medium' => $settings['image_logo_medium'] ?? $defaultLogo,
|
||||
'large' => $settings['image_logo'] ?? $defaultLogo,
|
||||
'small_dark' => $settings['image_logo_small_dark'] ?? $defaultLogo,
|
||||
'medium_dark' => $settings['image_logo_medium_dark'] ?? $defaultLogo,
|
||||
'large_dark' => $settings['image_logo_dark'] ?? $defaultLogo,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Valores de fallback si no hay base de datos.
|
||||
*/
|
||||
private function getDefaultAdminVars(?string $key = null): array
|
||||
{
|
||||
return $key
|
||||
? ($this->buildAdminVarsArray([])[$key] ?? null)
|
||||
: $this->buildAdminVarsArray([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Valores de fallback para customizer Vuexy.
|
||||
*/
|
||||
private function getDefaultVuexyVars(): array
|
||||
{
|
||||
return Config::get("{$this->namespace}.admin.vuexy", []);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Contracts\ApiRegistry;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Koneko\VuexyAdmin\Models\ExternalApi;
|
||||
|
||||
/**
|
||||
* Contrato para servicios de registro de APIs externas.
|
||||
*/
|
||||
interface ExternalApiRegistryInterface
|
||||
{
|
||||
public function all(): Collection;
|
||||
|
||||
public function active(): Collection;
|
||||
|
||||
public function groupByProvider(): Collection;
|
||||
|
||||
public function groupByModule(): Collection;
|
||||
|
||||
public function forModule(string $module): Collection;
|
||||
|
||||
public function forProvider(string $provider): Collection;
|
||||
|
||||
public function summary(): array;
|
||||
|
||||
public function slugifyName(string $name): string;
|
||||
|
||||
public function find(string $slug): ?ExternalApi;
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Contracts\Catalogs;
|
||||
|
||||
interface CatalogServiceInterface
|
||||
{
|
||||
public function catalogs(): array;
|
||||
public function exists(string $catalog): bool;
|
||||
public function getCatalog(string $catalog, string $searchTerm = '', array $options = []): array;
|
||||
public function getCatalogMeta(string $catalog): array;
|
||||
}
|
16
src/Application/Contracts/Enums/LabeledEnumInterface.php
Normal file
16
src/Application/Contracts/Enums/LabeledEnumInterface.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Contracts\Enums;
|
||||
|
||||
interface LabeledEnumInterface
|
||||
{
|
||||
public function label(): string;
|
||||
|
||||
public static function options(): array;
|
||||
public static function labels(): array;
|
||||
public static function values(): array;
|
||||
public static function validationRules(bool $required = false): array;
|
||||
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Contracts\Factories;
|
||||
|
||||
interface ProbabilisticAttributesFactoryInterface
|
||||
{
|
||||
public function maybe(int $percentage, mixed $value): mixed;
|
||||
public function maybeDefault(mixed $value): mixed;
|
||||
}
|
20
src/Application/Contracts/Files/ParsableFileInterface.php
Normal file
20
src/Application/Contracts/Files/ParsableFileInterface.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Contracts\Files;
|
||||
|
||||
interface ParsableFileInterface
|
||||
{
|
||||
/**
|
||||
* Parsea el archivo y devuelve un array de registros
|
||||
*
|
||||
* @throws \RuntimeException Cuando el archivo no puede ser procesado
|
||||
*/
|
||||
public function parse(string $path): array;
|
||||
|
||||
/**
|
||||
* Valida la estructura básica del archivo
|
||||
*/
|
||||
public function validate(string $path): bool;
|
||||
}
|
12
src/Application/Contracts/Flags/FlagEnumInterface.php
Normal file
12
src/Application/Contracts/Flags/FlagEnumInterface.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Contracts\Flags;
|
||||
|
||||
interface FlagEnumInterface
|
||||
{
|
||||
public function flagName(): string;
|
||||
public function description(): string;
|
||||
public static function modelClass(): string;
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Contracts\Loggers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
interface SecurityLoggerInterface
|
||||
{
|
||||
public function logEvent(
|
||||
string $type,
|
||||
?Request $request = null,
|
||||
?int $userId = null,
|
||||
array $payload = [],
|
||||
bool $isProxy = false
|
||||
): void;
|
||||
}
|
20
src/Application/Contracts/Loggers/SystemLoggerInterface.php
Normal file
20
src/Application/Contracts/Loggers/SystemLoggerInterface.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Contracts\Loggers;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Koneko\VuexyAdmin\Application\Enums\SystemLog\LogTriggerType;
|
||||
use Koneko\VuexyAdmin\Application\Enums\SystemLog\LogLevel;
|
||||
use Koneko\VuexyAdmin\Models\SystemLog;
|
||||
|
||||
interface SystemLoggerInterface
|
||||
{
|
||||
public function log(
|
||||
string|LogLevel $level,
|
||||
string $message,
|
||||
array $context = [],
|
||||
?Model $relatedModel = null,
|
||||
LogTriggerType $triggerType = LogTriggerType::System,
|
||||
?int $triggerId = null
|
||||
): SystemLog;
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Contracts\Loggers;
|
||||
|
||||
use Koneko\VuexyAdmin\Application\Enums\UserInteractions\InteractionSecurityLevel;
|
||||
use Koneko\VuexyAdmin\Models\UserInteraction;
|
||||
|
||||
interface UserInteractionLoggerInterface
|
||||
{
|
||||
public function record(
|
||||
string $action,
|
||||
array $context = [],
|
||||
InteractionSecurityLevel|string $security = 'normal',
|
||||
?string $livewireComponent = null
|
||||
): ?UserInteraction;
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Contracts\Modules;
|
||||
|
||||
use Koneko\VuexyAdmin\Application\Bootstrap\KonekoModule;
|
||||
|
||||
interface KonekoModuleServiceInterface
|
||||
{
|
||||
/**
|
||||
* Lista los módulos instalados en el sistema.
|
||||
*
|
||||
* @return KonekoModule[]
|
||||
*/
|
||||
public function listInstalled(): array;
|
||||
|
||||
/**
|
||||
* Lista los módulos disponibles en el marketplace oficial.
|
||||
*
|
||||
* @return array Módulos con metadatos
|
||||
*/
|
||||
public function listAvailable(): array;
|
||||
|
||||
/**
|
||||
* Obtiene un módulo instalado por su slug.
|
||||
*/
|
||||
public function get(string $slug): ?KonekoModule;
|
||||
|
||||
/**
|
||||
* Instala un módulo oficial desde el marketplace.
|
||||
*/
|
||||
public function installFromMarketplace(string $slug): bool;
|
||||
|
||||
/**
|
||||
* Instala un módulo desde una URL externa (ej. GitHub, repositorio privado).
|
||||
*/
|
||||
public function installFromUrl(string $url): bool;
|
||||
|
||||
/**
|
||||
* Activa un módulo previamente instalado.
|
||||
*/
|
||||
public function enable(string $slug): bool;
|
||||
|
||||
/**
|
||||
* Desactiva un módulo instalado (sin eliminarlo físicamente).
|
||||
*/
|
||||
public function disable(string $slug): bool;
|
||||
|
||||
/**
|
||||
* Desinstala un módulo: elimina archivos y entrada en base de datos.
|
||||
*/
|
||||
public function uninstall(string $slug): bool;
|
||||
|
||||
/**
|
||||
* Ejecuta migraciones de un módulo.
|
||||
*/
|
||||
public function migrate(string $slug): bool;
|
||||
|
||||
/**
|
||||
* Reversión de migraciones del módulo (rollback).
|
||||
*/
|
||||
public function rollback(string $slug): bool;
|
||||
|
||||
/**
|
||||
* Publica los assets del módulo.
|
||||
*/
|
||||
public function publishAssets(string $slug): bool;
|
||||
|
||||
/**
|
||||
* Elimina los assets previamente publicados.
|
||||
*/
|
||||
public function removeAssets(string $slug): bool;
|
||||
|
||||
/**
|
||||
* Sincroniza los módulos detectados en el filesystem con los registrados en la DB.
|
||||
*/
|
||||
public function syncFromModules(): bool;
|
||||
|
||||
/**
|
||||
* Lista todos los paquetes composer (instalados) que podrían ser módulos.
|
||||
*/
|
||||
public function detectAllComposerModules(): array;
|
||||
|
||||
/**
|
||||
* Obtiene metadatos extendidos de un módulo (instalado o no).
|
||||
*/
|
||||
public function getExtendedDetails(string $slug): array;
|
||||
}
|
14
src/Application/Contracts/Seeders/DataSeederInterface.php
Normal file
14
src/Application/Contracts/Seeders/DataSeederInterface.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Contracts\Seeders;
|
||||
|
||||
interface DataSeederInterface
|
||||
{
|
||||
public function run(array $options = []): void;
|
||||
|
||||
public function truncate(): void;
|
||||
|
||||
public function countSeededRecords(): int;
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Contracts\Settings;
|
||||
|
||||
use Koneko\VuexyAdmin\Models\Setting;
|
||||
|
||||
/**
|
||||
* Contrato para los servicios de gestión de Settings modulares.
|
||||
* Proporciona una API fluida para acceder y modificar configuraciones de manera modular.
|
||||
*/
|
||||
interface SettingsRepositoryInterface
|
||||
{
|
||||
public function get(string $key, ...$args): mixed;
|
||||
|
||||
public function set(string $key, mixed $value, ?int $userId = null, ...$args): ?Setting;
|
||||
|
||||
public function delete(string $key, ?int $userId = null): bool;
|
||||
|
||||
public function exists(string $key): bool;
|
||||
|
||||
public function getGroup(string $group, ?int $userId = null): array;
|
||||
|
||||
public function getComponent(string $component, ?int $userId = null): array;
|
||||
|
||||
public function deleteGroup(string $group, ?int $userId = null): int;
|
||||
|
||||
public function deleteComponent(string $component, ?int $userId = null): int;
|
||||
|
||||
public function listGroups(): array;
|
||||
|
||||
public function listComponents(): array;
|
||||
|
||||
public function currentNamespace(): string;
|
||||
|
||||
public function currentGroup(): string;
|
||||
|
||||
public function setContext(string $component, string $group): static;
|
||||
|
||||
public function setComponent(string $component): static;
|
||||
|
||||
public function setGroup(string $group): static;
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Contracts\Settings;
|
||||
|
||||
use Koneko\VuexyAdmin\Models\Setting;
|
||||
|
||||
/**
|
||||
* Contrato para los servicios de gestión de Settings modulares.
|
||||
* Proporciona una API fluida para acceder y modificar configuraciones de manera modular.
|
||||
*/
|
||||
interface SettingsRepositoryInterface
|
||||
{
|
||||
public function set(string $key, mixed $value): ?Setting;
|
||||
|
||||
public function get(string $key, mixed $default = null): mixed;
|
||||
|
||||
public function delete(string $key): bool;
|
||||
|
||||
public function exists(string $key): bool;
|
||||
|
||||
|
||||
public function markAsSystem(bool $state = true): static;
|
||||
public function markAsEncrypted(bool $state = true): static;
|
||||
public function markAsSensitive(bool $state = true): static;
|
||||
public function markAsEditable(bool $state = true): static;
|
||||
public function markAsActive(bool $state = true): static;
|
||||
|
||||
|
||||
public function withoutUsageTracking(): static;
|
||||
}
|
25
src/Application/Enums/ExternalApi/ApiAuthType.php
Normal file
25
src/Application/Enums/ExternalApi/ApiAuthType.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Enums\ExternalApi;
|
||||
|
||||
enum ApiAuthType: string
|
||||
{
|
||||
case ApiKey = 'api_key';
|
||||
case OAuth2 = 'oauth2';
|
||||
case JWT = 'jwt';
|
||||
case None = 'none';
|
||||
case BearerToken = 'bearer_token';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::ApiKey => 'API Key',
|
||||
self::OAuth2 => 'OAuth 2.0',
|
||||
self::JWT => 'JWT Token',
|
||||
self::None => 'Sin autenticación',
|
||||
self::BearerToken => 'Bearer Token',
|
||||
};
|
||||
}
|
||||
}
|
21
src/Application/Enums/ExternalApi/ApiEnvironment.php
Normal file
21
src/Application/Enums/ExternalApi/ApiEnvironment.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Enums\ExternalApi;
|
||||
|
||||
enum ApiEnvironment: string
|
||||
{
|
||||
case Development = 'dev';
|
||||
case Staging = 'staging';
|
||||
case Production = 'production';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Development => 'Desarrollo',
|
||||
self::Staging => 'Pruebas (Staging)',
|
||||
self::Production => 'Producción',
|
||||
};
|
||||
}
|
||||
}
|
31
src/Application/Enums/ExternalApi/ApiProvider.php
Normal file
31
src/Application/Enums/ExternalApi/ApiProvider.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Enums\ExternalApi;
|
||||
|
||||
enum ApiProvider: string
|
||||
{
|
||||
case Google = 'google';
|
||||
case Banxico = 'banxico';
|
||||
case SAT = 'sat';
|
||||
case Custom = 'custom';
|
||||
case Esys = 'esys';
|
||||
case Facebook = 'facebook';
|
||||
case Twitter = 'twitter';
|
||||
case TawkTo = 'tawk_to';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Google => 'Google',
|
||||
self::Banxico => 'Banxico',
|
||||
self::SAT => 'SAT (México)',
|
||||
self::Custom => 'Personalizado',
|
||||
self::Esys => 'ESYS',
|
||||
self::Facebook => 'Facebook',
|
||||
self::Twitter => 'Twitter',
|
||||
self::TawkTo => 'Tawk.to',
|
||||
};
|
||||
}
|
||||
}
|
9
src/Application/Enums/KeyVault/KeyVaultDriver.php
Normal file
9
src/Application/Enums/KeyVault/KeyVaultDriver.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Enums\KeyVault;
|
||||
|
||||
enum KeyVaultDriver: string {
|
||||
case LARAVEL = 'laravel';
|
||||
case DATABASE = 'database';
|
||||
case SERVICE = 'service';
|
||||
}
|
37
src/Application/Enums/Modules/ModulePackageSourceType.php
Normal file
37
src/Application/Enums/Modules/ModulePackageSourceType.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Enums\Modules;
|
||||
|
||||
enum ModulePackageSourceType: string
|
||||
{
|
||||
case Official = 'official'; // Curado por Koneko Marketplace
|
||||
case External = 'external'; // URL externa (ej. GitHub)
|
||||
case Custom = 'custom'; // Módulo propio/local
|
||||
case Zip = 'zip'; // Instalado desde archivo ZIP
|
||||
|
||||
/**
|
||||
* Devuelve la etiqueta legible para UI.
|
||||
*/
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Official => 'Marketplace Oficial',
|
||||
self::External => 'Repositorio Externo',
|
||||
self::Custom => 'Componente Interno',
|
||||
self::Zip => 'Instalado desde ZIP',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Devuelve un ícono representativo.
|
||||
*/
|
||||
public function icon(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Official => 'ti ti-shield-check',
|
||||
self::External => 'ti ti-world-www',
|
||||
self::Custom => 'ti ti-tools',
|
||||
self::Zip => 'ti ti-file-zip',
|
||||
};
|
||||
}
|
||||
}
|
14
src/Application/Enums/Notifications/NotificationCahennel.php
Normal file
14
src/Application/Enums/Notifications/NotificationCahennel.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Enums\Notifications;
|
||||
|
||||
enum NotificationCahennel: string
|
||||
{
|
||||
case Toast = 'toast';
|
||||
case Push = 'push';
|
||||
case WebSocket = 'websocket';
|
||||
case Email = 'email';
|
||||
case InApp = 'inapp';
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Enums\PermissionGroup;
|
||||
|
||||
enum PermissionGroupType: string
|
||||
{
|
||||
case Module = 'module';
|
||||
case RootGroup = 'root_group';
|
||||
case SubGroup = 'sub_group';
|
||||
case ExternalGroup = 'external_group';
|
||||
}
|
95
src/Application/Enums/Permissions/PermissionAction.php
Normal file
95
src/Application/Enums/Permissions/PermissionAction.php
Normal file
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Enums\Permissions;
|
||||
|
||||
use Illuminate\Support\Facades\App;
|
||||
|
||||
enum PermissionAction: string
|
||||
{
|
||||
case View = 'view';
|
||||
case Create = 'create';
|
||||
case Update = 'update';
|
||||
case Delete = 'delete';
|
||||
case Approve = 'approve';
|
||||
case Cancel = 'cancel';
|
||||
case Close = 'close';
|
||||
case Reopen = 'reopen';
|
||||
case Duplicate = 'duplicate';
|
||||
case Import = 'import';
|
||||
case Export = 'export';
|
||||
case Print = 'print';
|
||||
case Email = 'email';
|
||||
case Sync = 'sync';
|
||||
case Configure = 'configure';
|
||||
case Allow = 'allow';
|
||||
case Assign = 'assign';
|
||||
case Stamp = 'stamp';
|
||||
case Install = 'install';
|
||||
case Clean = 'clean';
|
||||
case Publish = 'publish';
|
||||
case Archive = 'archive';
|
||||
case Feature = 'feature';
|
||||
|
||||
public function label(?string $locale = null): ?string
|
||||
{
|
||||
$locale ??= App::getLocale();
|
||||
|
||||
$labels = match ($locale) {
|
||||
'es' => [
|
||||
self::View->value => 'Ver',
|
||||
self::Create->value => 'Crear',
|
||||
self::Update->value => 'Editar',
|
||||
self::Delete->value => 'Eliminar',
|
||||
self::Approve->value => 'Aprobar',
|
||||
self::Cancel->value => 'Cancelar',
|
||||
self::Close->value => 'Cerrar',
|
||||
self::Reopen->value => 'Reabrir',
|
||||
self::Duplicate->value => 'Duplicar',
|
||||
self::Import->value => 'Importar',
|
||||
self::Export->value => 'Exportar',
|
||||
self::Print->value => 'Imprimir',
|
||||
self::Email->value => 'Correo',
|
||||
self::Sync->value => 'Sincronizar',
|
||||
self::Configure->value => 'Configurar',
|
||||
self::Allow->value => 'Autorizar',
|
||||
self::Assign->value => 'Asignar',
|
||||
self::Stamp->value => 'Timbrar',
|
||||
self::Install->value => 'Instalar',
|
||||
self::Clean->value => 'Limpiar',
|
||||
self::Publish->value => 'Publicar',
|
||||
self::Archive->value => 'Archivar',
|
||||
self::Feature->value => 'Destacar',
|
||||
],
|
||||
'en' => [
|
||||
self::View->value => 'View',
|
||||
self::Create->value => 'Create',
|
||||
self::Update->value => 'Update',
|
||||
self::Delete->value => 'Delete',
|
||||
self::Approve->value => 'Approve',
|
||||
self::Cancel->value => 'Cancel',
|
||||
self::Close->value => 'Close',
|
||||
self::Reopen->value => 'Reopen',
|
||||
self::Duplicate->value => 'Duplicate',
|
||||
self::Import->value => 'Import',
|
||||
self::Export->value => 'Export',
|
||||
self::Print->value => 'Print',
|
||||
self::Email->value => 'Email',
|
||||
self::Sync->value => 'Sync',
|
||||
self::Configure->value => 'Configure',
|
||||
self::Allow->value => 'Authorize',
|
||||
self::Assign->value => 'Assign',
|
||||
self::Stamp->value => 'Stamp',
|
||||
self::Install->value => 'Install',
|
||||
self::Clean->value => 'Clean',
|
||||
self::Publish->value => 'Publish',
|
||||
self::Archive->value => 'Archive',
|
||||
self::Feature->value => 'Feature',
|
||||
],
|
||||
default => [] // fallback vacío
|
||||
};
|
||||
|
||||
return $labels[$this->value] ?? ucfirst($this->value);
|
||||
}
|
||||
}
|
24
src/Application/Enums/SecurityEvents/SecurityEventStatus.php
Normal file
24
src/Application/Enums/SecurityEvents/SecurityEventStatus.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Enums\SecurityEvents;
|
||||
|
||||
enum SecurityEventStatus: string
|
||||
{
|
||||
case NEW = 'new';
|
||||
case RESOLVED = 'resolved';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::NEW => 'Nuevo',
|
||||
self::RESOLVED => 'Resuelto',
|
||||
};
|
||||
}
|
||||
|
||||
public static function options(): array
|
||||
{
|
||||
return array_column(self::cases(), 'label', 'value');
|
||||
}
|
||||
}
|
25
src/Application/Enums/SecurityEvents/SecurityEventType.php
Normal file
25
src/Application/Enums/SecurityEvents/SecurityEventType.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Enums\SecurityEvents;
|
||||
|
||||
enum SecurityEventType: string
|
||||
{
|
||||
case LOGIN_FAILED = 'failed_login_attempt';
|
||||
case LOGIN_SUCCESS = 'login_success';
|
||||
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::LOGIN_FAILED => 'Inicio fallido',
|
||||
self::LOGIN_SUCCESS => 'Inicio exitoso',
|
||||
};
|
||||
}
|
||||
|
||||
public static function options(): array
|
||||
{
|
||||
return array_column(self::cases(), 'label', 'value');
|
||||
}
|
||||
}
|
15
src/Application/Enums/Settings/SettingEnvironment.php
Normal file
15
src/Application/Enums/Settings/SettingEnvironment.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Enums\Settings;
|
||||
|
||||
use Koneko\VuexyAdmin\Support\Traits\Enums\HasEnumHelpers;
|
||||
|
||||
enum SettingEnvironment: string
|
||||
{
|
||||
use HasEnumHelpers;
|
||||
|
||||
case PROD = 'prod';
|
||||
case DEV = 'dev';
|
||||
case STAGING = 'staging';
|
||||
case TEST = 'test';
|
||||
}
|
16
src/Application/Enums/Settings/SettingScope.php
Normal file
16
src/Application/Enums/Settings/SettingScope.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Enums\Settings;
|
||||
|
||||
use Koneko\VuexyAdmin\Support\Traits\Enums\HasEnumHelpers;
|
||||
|
||||
enum SettingScope: string
|
||||
{
|
||||
use HasEnumHelpers;
|
||||
|
||||
case GLOBAL = 'global';
|
||||
case TENANT = 'tenant';
|
||||
case BRANCH = 'branch';
|
||||
case USER = 'user';
|
||||
case GUEST = 'guest';
|
||||
}
|
17
src/Application/Enums/Settings/SettingValueType.php
Normal file
17
src/Application/Enums/Settings/SettingValueType.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Enums\Settings;
|
||||
|
||||
use Koneko\VuexyAdmin\Support\Traits\Enums\HasEnumHelpers;
|
||||
|
||||
enum SettingValueType: string
|
||||
{
|
||||
use HasEnumHelpers;
|
||||
|
||||
case STRING = 'string';
|
||||
case INTEGER = 'integer';
|
||||
case BOOLEAN = 'boolean';
|
||||
case FLOAT = 'float';
|
||||
case TEXT = 'text';
|
||||
case BINARY = 'binary';
|
||||
}
|
13
src/Application/Enums/SystemLog/LogLevel.php
Normal file
13
src/Application/Enums/SystemLog/LogLevel.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Enums\SystemLog;
|
||||
|
||||
enum LogLevel: string
|
||||
{
|
||||
case Info = 'info';
|
||||
case Warning = 'warning';
|
||||
case Error = 'error';
|
||||
case Debug = 'debug';
|
||||
}
|
17
src/Application/Enums/SystemLog/LogTriggerType.php
Normal file
17
src/Application/Enums/SystemLog/LogTriggerType.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Enums\SystemLog;
|
||||
|
||||
enum LogTriggerType: string
|
||||
{
|
||||
case User = 'user';
|
||||
case Cron = 'cronjob';
|
||||
case Seeder = 'seeder';
|
||||
case Listener = 'listener';
|
||||
case Webhook = 'webhook';
|
||||
case System = 'system';
|
||||
case Api = 'api';
|
||||
case Job = 'job';
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Enums\SystemNotifications;
|
||||
|
||||
enum SystemNotificationPriority: string
|
||||
{
|
||||
case Low = 'low';
|
||||
case Medium = 'medium';
|
||||
case High = 'high';
|
||||
case Critical = 'critical';
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Enums\SystemNotifications;
|
||||
|
||||
enum SystemNotificationScope: string
|
||||
{
|
||||
case Admin = 'admin';
|
||||
case Frontend = 'frontend';
|
||||
case Both = 'both';
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Enums\SystemNotifications;
|
||||
|
||||
enum SystemNotificationStyle: string
|
||||
{
|
||||
case Toast = 'toast';
|
||||
case Banner = 'banner';
|
||||
case Modal = 'modal';
|
||||
case Inline = 'inline';
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Enums\SystemNotifications;
|
||||
|
||||
enum SystemNotificationType: string
|
||||
{
|
||||
case Info = 'info';
|
||||
case Success = 'success';
|
||||
case Warning = 'warning';
|
||||
case Danger = 'danger';
|
||||
case Promo = 'promo';
|
||||
}
|
35
src/Application/Enums/User/UserBaseFlags.php
Normal file
35
src/Application/Enums/User/UserBaseFlags.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Enums\User;
|
||||
|
||||
use Koneko\VuexyAdmin\Models\User;
|
||||
use Koneko\VuexyAdmin\Support\Traits\Enums\FlagEnumTrait;
|
||||
|
||||
enum UserBaseFlags: string
|
||||
{
|
||||
use FlagEnumTrait;
|
||||
|
||||
case IS_USER = 'is_user';
|
||||
|
||||
public static function modelClass(): string
|
||||
{
|
||||
return User::class;
|
||||
}
|
||||
|
||||
public static function getDescription(self $case): string
|
||||
{
|
||||
return match($case) {
|
||||
self::IS_USER => 'Usuario de sistema',
|
||||
};
|
||||
}
|
||||
|
||||
public function describeActiveFlags(): array
|
||||
{
|
||||
return collect(static::getRegisteredFlags())
|
||||
->filter(fn ($desc, $flag) => $this->hasFlag($flag))
|
||||
->mapWithKeys(fn ($desc, $flag) => [$flag => $desc])
|
||||
->all();
|
||||
}
|
||||
}
|
19
src/Application/Enums/User/UserStatus.php
Normal file
19
src/Application/Enums/User/UserStatus.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Enums\User;
|
||||
|
||||
enum UserStatus: int
|
||||
{
|
||||
case ENABLED = 1;
|
||||
case DISABLED = 0;
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::ENABLED => 'Activo',
|
||||
self::DISABLED => 'Deshabilitado',
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Enums\UserInteractions;
|
||||
|
||||
enum InteractionSecurityLevel: string
|
||||
{
|
||||
case Normal = 'normal';
|
||||
case Sensible = 'sensible';
|
||||
case Critical = 'critical';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Normal => 'Normal',
|
||||
self::Sensible => 'Sensible',
|
||||
self::Critical => 'Crítico',
|
||||
};
|
||||
}
|
||||
|
||||
public function badgeClass(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Normal => 'badge bg-light text-dark',
|
||||
self::Sensible => 'badge bg-warning text-dark',
|
||||
self::Critical => 'badge bg-danger text-white',
|
||||
};
|
||||
}
|
||||
}
|
19
src/Application/Events/Settings/SettingChanged.php
Normal file
19
src/Application/Events/Settings/SettingChanged.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Events\Settings;
|
||||
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
|
||||
class SettingChanged
|
||||
{
|
||||
use Dispatchable;
|
||||
|
||||
/**
|
||||
* @param string $key Clave completa del setting (ej. 'koneko.admin.site.logo')
|
||||
* @param string $namespace Namespace base (ej. 'koneko.admin.site.')
|
||||
* @param int|null $userId Usuario relacionado si es setting scoped, null si es global
|
||||
*/
|
||||
public function __construct(public string $key) {}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Events\Settings;
|
||||
|
||||
class VuexyCustomizerSettingsUpdated
|
||||
{
|
||||
public function __construct(public array $settings = []) {}
|
||||
}
|
24
src/Application/Factories/FactoryExtensionRegistry.php
Normal file
24
src/Application/Factories/FactoryExtensionRegistry.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Factories;
|
||||
|
||||
/**
|
||||
* 📚 FactoryExtensionRegistry
|
||||
*
|
||||
*/
|
||||
class FactoryExtensionRegistry
|
||||
{
|
||||
protected static array $registeredTraits = [];
|
||||
|
||||
public static function registerFactoryTrait(string $factoryClass, string $traitClass): void
|
||||
{
|
||||
static::$registeredTraits[$factoryClass][] = $traitClass;
|
||||
}
|
||||
|
||||
public static function getFactoryTraits(string $factoryClass): array
|
||||
{
|
||||
return static::$registeredTraits[$factoryClass] ?? [];
|
||||
}
|
||||
}
|
74
src/Application/Helpers/KonekoCatalogHelper.php
Normal file
74
src/Application/Helpers/KonekoCatalogHelper.php
Normal file
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Support\Helpers;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class KonekoCatalogHelper
|
||||
{
|
||||
public static function ajaxFlexibleResponse(Builder $query, array $options = []): JsonResponse
|
||||
{
|
||||
$id = $options['id'] ?? null;
|
||||
$searchTerm = $options['searchTerm'] ?? null;
|
||||
$limit = $options['limit'] ?? 20;
|
||||
$keyField = $options['keyField'] ?? 'id';
|
||||
$valueField = $options['valueField'] ?? 'name';
|
||||
$responseType = $options['responseType'] ?? 'select2';
|
||||
$customFilters = $options['filters'] ?? [];
|
||||
|
||||
// Si se pasa un ID, devolver un registro completo
|
||||
if ($id) {
|
||||
$data = $query->find($id);
|
||||
return response()->json($data);
|
||||
}
|
||||
|
||||
// Aplicar filtros personalizados
|
||||
foreach ($customFilters as $field => $value) {
|
||||
if (!is_null($value)) {
|
||||
$query->where($field, $value);
|
||||
}
|
||||
}
|
||||
|
||||
// Aplicar filtro de búsqueda si hay searchTerm
|
||||
if ($searchTerm) {
|
||||
$query->where($valueField, 'like', '%' . $searchTerm . '%');
|
||||
}
|
||||
|
||||
// Limitar resultados si el límite no es falso
|
||||
if ($limit > 0) {
|
||||
$query->limit($limit);
|
||||
}
|
||||
|
||||
$results = $query->get([$keyField, $valueField]);
|
||||
|
||||
// Devolver según el tipo de respuesta
|
||||
switch ($responseType) {
|
||||
case 'keyValue':
|
||||
$data = $results->pluck($valueField, $keyField)->toArray();
|
||||
break;
|
||||
|
||||
case 'select2':
|
||||
$data = $results->map(function ($item) use ($keyField, $valueField) {
|
||||
return [
|
||||
'id' => $item->{$keyField},
|
||||
'text' => $item->{$valueField},
|
||||
];
|
||||
})->toArray();
|
||||
break;
|
||||
|
||||
default:
|
||||
$data = $results->map(function ($item) use ($keyField, $valueField) {
|
||||
return [
|
||||
'id' => $item->{$keyField},
|
||||
'text' => $item->{$valueField},
|
||||
];
|
||||
})->toArray();
|
||||
break;
|
||||
}
|
||||
|
||||
return response()->json($data);
|
||||
}
|
||||
}
|
212
src/Application/Helpers/VuexyHelper.php
Normal file
212
src/Application/Helpers/VuexyHelper.php
Normal file
@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Helpers;
|
||||
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class VuexyHelper
|
||||
{
|
||||
public const NAMESPACE = 'koneko';
|
||||
|
||||
public static function appClasses()
|
||||
{
|
||||
$data = config('koneko.admin.vuexy');
|
||||
|
||||
// default data array
|
||||
$DefaultData = [
|
||||
'myLayout' => 'vertical',
|
||||
'myTheme' => 'theme-default',
|
||||
'myStyle' => 'light',
|
||||
'myRTLSupport' => false,
|
||||
'myRTLMode' => false,
|
||||
'hasCustomizer' => true,
|
||||
'showDropdownOnHover' => true,
|
||||
'displayCustomizer' => true,
|
||||
'contentLayout' => 'compact',
|
||||
'headerType' => 'fixed',
|
||||
'navbarType' => 'fixed',
|
||||
'menuFixed' => true,
|
||||
'menuCollapsed' => false,
|
||||
'footerFixed' => false,
|
||||
'customizerControls' => [
|
||||
'rtl',
|
||||
'style',
|
||||
'headerType',
|
||||
'contentLayout',
|
||||
'layoutCollapsed',
|
||||
'showDropdownOnHover',
|
||||
'layoutNavbarOptions',
|
||||
'themes',
|
||||
],
|
||||
// 'defaultLanguage'=>'en',
|
||||
];
|
||||
|
||||
// if any key missing of array from custom.php file it will be merge and set a default value from dataDefault array and store in data variable
|
||||
$data = array_merge($DefaultData, $data);
|
||||
|
||||
// All options available in the template
|
||||
$allOptions = [
|
||||
'myLayout' => ['vertical', 'horizontal', 'blank', 'front'],
|
||||
'menuCollapsed' => [true, false],
|
||||
'hasCustomizer' => [true, false],
|
||||
'showDropdownOnHover' => [true, false],
|
||||
'displayCustomizer' => [true, false],
|
||||
'contentLayout' => ['compact', 'wide'],
|
||||
'headerType' => ['fixed', 'static'],
|
||||
'navbarType' => ['fixed', 'static', 'hidden'],
|
||||
'myStyle' => ['light', 'dark', 'system'],
|
||||
'myTheme' => ['theme-default', 'theme-bordered', 'theme-semi-dark'],
|
||||
'myRTLSupport' => [true, false],
|
||||
'myRTLMode' => [true, false],
|
||||
'menuFixed' => [true, false],
|
||||
'footerFixed' => [true, false],
|
||||
'customizerControls' => [],
|
||||
// 'defaultLanguage'=>array('en'=>'en','fr'=>'fr','de'=>'de','ar'=>'ar'),
|
||||
];
|
||||
|
||||
//if myLayout value empty or not match with default options in custom.php config file then set a default value
|
||||
foreach ($allOptions as $key => $value) {
|
||||
if (array_key_exists($key, $DefaultData)) {
|
||||
if (gettype($DefaultData[$key]) === gettype($data[$key])) {
|
||||
// data key should be string
|
||||
if (is_string($data[$key])) {
|
||||
// data key should not be empty
|
||||
if (isset($data[$key]) && $data[$key] !== null) {
|
||||
// data key should not be exist inside allOptions array's sub array
|
||||
if (!array_key_exists($data[$key], $value)) {
|
||||
// ensure that passed value should be match with any of allOptions array value
|
||||
$result = array_search($data[$key], $value, true);
|
||||
if (empty($result) && $result !== 0) {
|
||||
$data[$key] = $DefaultData[$key];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if data key not set or
|
||||
$data[$key] = $DefaultData[$key];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$data[$key] = $DefaultData[$key];
|
||||
}
|
||||
}
|
||||
}
|
||||
$styleVal = $data['myStyle'] == "dark" ? "dark" : "light";
|
||||
$styleUpdatedVal = $data['myStyle'] == "dark" ? "dark" : $data['myStyle'];
|
||||
// Determine if the layout is admin or front based on cookies
|
||||
$layoutName = $data['myLayout'];
|
||||
$isAdmin = Str::contains($layoutName, 'front') ? false : true;
|
||||
|
||||
$modeCookieName = $isAdmin ? 'admin-mode' : 'front-mode';
|
||||
$colorPrefCookieName = $isAdmin ? 'admin-colorPref' : 'front-colorPref';
|
||||
|
||||
// Determine style based on cookies, only if not 'blank-layout'
|
||||
if ($layoutName !== 'blank') {
|
||||
if (isset($_COOKIE[$modeCookieName])) {
|
||||
$styleVal = $_COOKIE[$modeCookieName];
|
||||
if ($styleVal === 'system') {
|
||||
$styleVal = isset($_COOKIE[$colorPrefCookieName]) ? $_COOKIE[$colorPrefCookieName] : 'light';
|
||||
}
|
||||
$styleUpdatedVal = $_COOKIE[$modeCookieName];
|
||||
}
|
||||
}
|
||||
|
||||
isset($_COOKIE['theme']) ? $themeVal = $_COOKIE['theme'] : $themeVal = $data['myTheme'];
|
||||
|
||||
$directionVal = isset($_COOKIE['direction']) ? ($_COOKIE['direction'] === "true" ? 'rtl' : 'ltr') : $data['myRTLMode'];
|
||||
|
||||
//layout classes
|
||||
$layoutClasses = [
|
||||
'layout' => $data['myLayout'],
|
||||
'theme' => $themeVal,
|
||||
'themeOpt' => $data['myTheme'],
|
||||
'style' => $styleVal,
|
||||
'styleOpt' => $data['myStyle'],
|
||||
'styleOptVal' => $styleUpdatedVal,
|
||||
'rtlSupport' => $data['myRTLSupport'],
|
||||
'rtlMode' => $data['myRTLMode'],
|
||||
'textDirection' => $directionVal, //$data['myRTLMode'],
|
||||
'menuCollapsed' => $data['menuCollapsed'],
|
||||
'hasCustomizer' => $data['hasCustomizer'],
|
||||
'showDropdownOnHover' => $data['showDropdownOnHover'],
|
||||
'displayCustomizer' => $data['displayCustomizer'],
|
||||
'contentLayout' => $data['contentLayout'],
|
||||
'headerType' => $data['headerType'],
|
||||
'navbarType' => $data['navbarType'],
|
||||
'menuFixed' => $data['menuFixed'],
|
||||
'footerFixed' => $data['footerFixed'],
|
||||
'customizerControls' => $data['customizerControls'],
|
||||
];
|
||||
|
||||
// sidebar Collapsed
|
||||
if ($layoutClasses['menuCollapsed'] == true) {
|
||||
$layoutClasses['menuCollapsed'] = 'layout-menu-collapsed';
|
||||
}
|
||||
|
||||
// Header Type
|
||||
if ($layoutClasses['headerType'] == 'fixed') {
|
||||
$layoutClasses['headerType'] = 'layout-menu-fixed';
|
||||
}
|
||||
// Navbar Type
|
||||
if ($layoutClasses['navbarType'] == 'fixed') {
|
||||
$layoutClasses['navbarType'] = 'layout-navbar-fixed';
|
||||
} elseif ($layoutClasses['navbarType'] == 'static') {
|
||||
$layoutClasses['navbarType'] = '';
|
||||
} else {
|
||||
$layoutClasses['navbarType'] = 'layout-navbar-hidden';
|
||||
}
|
||||
|
||||
// Menu Fixed
|
||||
if ($layoutClasses['menuFixed'] == true) {
|
||||
$layoutClasses['menuFixed'] = 'layout-menu-fixed';
|
||||
}
|
||||
|
||||
// Footer Fixed
|
||||
if ($layoutClasses['footerFixed'] == true) {
|
||||
$layoutClasses['footerFixed'] = 'layout-footer-fixed';
|
||||
}
|
||||
|
||||
// RTL Supported template
|
||||
if ($layoutClasses['rtlSupport'] == true) {
|
||||
$layoutClasses['rtlSupport'] = '/rtl';
|
||||
}
|
||||
|
||||
// RTL Layout/Mode
|
||||
if ($layoutClasses['rtlMode'] == true) {
|
||||
$layoutClasses['rtlMode'] = 'rtl';
|
||||
$layoutClasses['textDirection'] = isset($_COOKIE['direction']) ? ($_COOKIE['direction'] === "true" ? 'rtl' : 'ltr') : 'rtl';
|
||||
} else {
|
||||
$layoutClasses['rtlMode'] = 'ltr';
|
||||
$layoutClasses['textDirection'] = isset($_COOKIE['direction']) && $_COOKIE['direction'] === "true" ? 'rtl' : 'ltr';
|
||||
}
|
||||
|
||||
// Show DropdownOnHover for Horizontal Menu
|
||||
if ($layoutClasses['showDropdownOnHover'] == true) {
|
||||
$layoutClasses['showDropdownOnHover'] = true;
|
||||
} else {
|
||||
$layoutClasses['showDropdownOnHover'] = false;
|
||||
}
|
||||
|
||||
// To hide/show display customizer UI, not js
|
||||
if ($layoutClasses['displayCustomizer'] == true) {
|
||||
$layoutClasses['displayCustomizer'] = true;
|
||||
} else {
|
||||
$layoutClasses['displayCustomizer'] = false;
|
||||
}
|
||||
|
||||
return $layoutClasses;
|
||||
}
|
||||
|
||||
public static function updatePageConfig($pageConfigs)
|
||||
{
|
||||
if (isset($pageConfigs)) {
|
||||
if (count($pageConfigs) > 0) {
|
||||
foreach ($pageConfigs as $config => $val) {
|
||||
Config::set('koneko.admin.vuexy.' . $config, $val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
36
src/Application/Helpers/VuexyNotifyHelper.php
Normal file
36
src/Application/Helpers/VuexyNotifyHelper.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Helpers;
|
||||
|
||||
class VuexyNotifyHelper
|
||||
{
|
||||
public static function flash(string $message, string $type = 'info', string $target = 'body', int $delay = 5000): void
|
||||
{
|
||||
session()->flash('vuexy_notification', [
|
||||
'type' => $type, // primary, success, danger, warning, info, dark
|
||||
'message' => $message,
|
||||
'target' => $target,
|
||||
'delay' => $delay,
|
||||
]);
|
||||
}
|
||||
|
||||
public static function success(string $message, string $target = 'body', int $delay = 5000): void
|
||||
{
|
||||
static::flash($message, 'success', $target, $delay);
|
||||
}
|
||||
|
||||
public static function error(string $message, string $target = 'body', int $delay = 5000): void
|
||||
{
|
||||
static::flash($message, 'danger', $target, $delay);
|
||||
}
|
||||
|
||||
public static function warning(string $message, string $target = 'body', int $delay = 5000): void
|
||||
{
|
||||
static::flash($message, 'warning', $target, $delay);
|
||||
}
|
||||
|
||||
public static function info(string $message, string $target = 'body', int $delay = 5000): void
|
||||
{
|
||||
static::flash($message, 'info', $target, $delay);
|
||||
}
|
||||
}
|
37
src/Application/Helpers/VuexyToastrHelper.php
Normal file
37
src/Application/Helpers/VuexyToastrHelper.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Helpers;
|
||||
|
||||
class VuexyToastrHelper
|
||||
{
|
||||
public static function flash(string $message, string $type = 'info', int $delay = 5000): void
|
||||
{
|
||||
session()->flash('vuexy_toastr', [
|
||||
'type' => $type, // success, info, warning, error
|
||||
'message' => $message,
|
||||
'delay' => $delay,
|
||||
]);
|
||||
}
|
||||
|
||||
public static function success(string $message, int $delay = 5000): void
|
||||
{
|
||||
static::flash($message, 'success', $delay);
|
||||
}
|
||||
|
||||
public static function error(string $message, int $delay = 5000): void
|
||||
{
|
||||
static::flash($message, 'error', $delay);
|
||||
}
|
||||
|
||||
public static function warning(string $message, int $delay = 5000): void
|
||||
{
|
||||
static::flash($message, 'warning', $delay);
|
||||
}
|
||||
|
||||
public static function info(string $message, int $delay = 5000): void
|
||||
{
|
||||
static::flash($message, 'info', $delay);
|
||||
}
|
||||
}
|
64
src/Application/Http/Controllers/AuditoriaController.php
Normal file
64
src/Application/Http/Controllers/AuditoriaController.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Http\Controllers;
|
||||
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\View\View;
|
||||
use Koneko\VuexyAdmin\Application\UX\ConfigBuilders\System\SecurityEventsTableConfigBuilder;
|
||||
use Koneko\VuexyAdmin\Application\UX\ConfigBuilders\Users\UserLoginTableConfigBuilder;
|
||||
|
||||
/**
|
||||
* Controlador para la gestión de funcionalidades administrativas de Vuexy
|
||||
*/
|
||||
class AuditoriaController extends Controller
|
||||
{
|
||||
/**
|
||||
* Muestra el listado de accesos al sistema (Bootstrap Table AJAX or View).
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse|\Illuminate\View\View
|
||||
*/
|
||||
public function usersAuthLogs(Request $request): JsonResponse|View
|
||||
{
|
||||
if ($request->ajax()) {
|
||||
$builder = app(UserLoginTableConfigBuilder::class)->getQueryBuilder($request);
|
||||
|
||||
return $builder->getJson();
|
||||
}
|
||||
|
||||
return view('vuexy-admin::audit.users-auth-logs.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Muestra el listado de eventos de auditoría (Bootstrap Table AJAX or View).
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse|\Illuminate\View\View
|
||||
*/
|
||||
public function securityEvents(Request $request): JsonResponse|View
|
||||
{
|
||||
if ($request->ajax()) {
|
||||
$builder = app(SecurityEventsTableConfigBuilder::class)->getQueryBuilder($request);
|
||||
|
||||
return $builder->getJson();
|
||||
}
|
||||
|
||||
return view('vuexy-admin::audit.security-events.index');
|
||||
}
|
||||
|
||||
public function laravelLogs(Request $request): JsonResponse|View
|
||||
{
|
||||
if ($request->ajax()) {
|
||||
$builder = app(laravelLogsTableConfigBuilder::class)->getQueryBuilder($request);
|
||||
|
||||
return $builder->getJson();
|
||||
}
|
||||
|
||||
return view('vuexy-admin::audit.laravel-logs.index');
|
||||
}
|
||||
|
||||
}
|
73
src/Application/Http/Controllers/CacheController.php
Normal file
73
src/Application/Http/Controllers/CacheController.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Http\Controllers;
|
||||
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Koneko\VuexyAdmin\Application\Cache\CacheConfigService;
|
||||
|
||||
class CacheController extends Controller
|
||||
{
|
||||
public function laravelIndex(): View
|
||||
{
|
||||
return view('vuexy-admin::tools.cache.laravel.index');
|
||||
}
|
||||
|
||||
public function redisIndex(CacheConfigService $cacheConfigService): View
|
||||
{
|
||||
$configCache = $cacheConfigService->getConfig();
|
||||
|
||||
return view('vuexy-admin::tools.cache.redis.index', compact('configCache'));
|
||||
}
|
||||
|
||||
public function memcacheIndex(CacheConfigService $cacheConfigService): View
|
||||
{
|
||||
$configCache = $cacheConfigService->getConfig();
|
||||
|
||||
return view('vuexy-admin::tools.cache.memcache.index', compact('configCache'));
|
||||
}
|
||||
|
||||
public function generateConfigCache(): JsonResponse
|
||||
{
|
||||
try {
|
||||
// Lógica para generar cache
|
||||
Artisan::call('config:cache');
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Cache generado correctamente.']);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['success' => false, 'message' => 'Error al generar el cache.', 'error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function generateRouteCache(): JsonResponse
|
||||
{
|
||||
try {
|
||||
// Lógica para generar cache de rutas
|
||||
Artisan::call('route:cache');
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Cache de rutas generado correctamente.']);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['success' => false, 'message' => 'Error al generar el cache de rutas.', 'error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function sessionsIndex(): View
|
||||
{
|
||||
return view('vuexy-admin::tools.cache.sessions.index');
|
||||
}
|
||||
|
||||
public function generateViteAssetsCache(): View
|
||||
{
|
||||
return view('vuexy-admin::tools.cache.vite-assets.index');
|
||||
}
|
||||
|
||||
public function generateTtlCache(): View
|
||||
{
|
||||
return view('vuexy-admin::tools.cache.ttl.index');
|
||||
}
|
||||
}
|
47
src/Application/Http/Controllers/CatalogAjaxController.php
Normal file
47
src/Application/Http/Controllers/CatalogAjaxController.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Koneko\VuexyAdmin\Application\Bootstrap\Extenders\Catalog\CatalogModuleRegistry;
|
||||
|
||||
class CatalogAjaxController extends Controller
|
||||
{
|
||||
/**
|
||||
* Consulta un catálogo de un componente vía AJAX.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param string $component Slug del componente, ej. 'sat-catalogs'
|
||||
* @param string $catalog Nombre del catálogo, ej. 'forma_pago'
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function fetch(Request $request, string $component, string $catalog): JsonResponse
|
||||
{
|
||||
$service = CatalogModuleRegistry::get($component);
|
||||
|
||||
if (!$service || !$service->exists($catalog)) {
|
||||
return response()->json([
|
||||
'error' => 'Catálogo no disponible o componente no registrado.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$search = $request->get('searchTerm', '');
|
||||
$options = $request->except(['searchTerm']);
|
||||
|
||||
try {
|
||||
$results = $service->getCatalog($catalog, $search, $options);
|
||||
|
||||
return response()->json($results);
|
||||
} catch (\Throwable $e) {
|
||||
report($e); // log opcional
|
||||
return response()->json([
|
||||
'error' => 'Error al consultar el catálogo.',
|
||||
'message' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Http\Controllers;
|
||||
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Http\{JsonResponse,Request};
|
||||
use Illuminate\View\View;
|
||||
use Koneko\VuexyAdmin\Application\UX\ConfigBuilders\System\EnvironmentVarsTableConfigBuilder;
|
||||
|
||||
class GeneralSettingsController extends Controller
|
||||
{
|
||||
/**
|
||||
* Muestra la vista de configuraciones generales
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function webInterface(): View
|
||||
{
|
||||
return view('vuexy-admin::settings.web-interface.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Muestra la vista de configuraciones de interfaz
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function vuexyInterface(): View
|
||||
{
|
||||
//dd(Livewire::class);
|
||||
|
||||
return view('vuexy-admin::settings.vuexy-interface.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Muestra la vista de configuraciones SMTP
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function smtpSettings(): View
|
||||
{
|
||||
return view('vuexy-admin::settings.smtp-settings.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a listing of the resource (Bootstrap Table AJAX or View).
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function environmentVars(Request $request): JsonResponse|View
|
||||
{
|
||||
if ($request->ajax()) {
|
||||
$builder = app(EnvironmentVarsTableConfigBuilder::class)->getQueryBuilder($request);
|
||||
|
||||
return $builder->getJson();
|
||||
}
|
||||
|
||||
return view('vuexy-admin::settings.environment-vars.index');
|
||||
}
|
||||
}
|
35
src/Application/Http/Controllers/HomeController.php
Normal file
35
src/Application/Http/Controllers/HomeController.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Http\Controllers;
|
||||
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
return view('vuexy-admin::pages.home');
|
||||
}
|
||||
|
||||
public function about(): View
|
||||
{
|
||||
return view('vuexy-admin::pages.about');
|
||||
}
|
||||
|
||||
public function comingsoon(): View
|
||||
{
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
return view('vuexy-admin::pages.comingsoon', compact('pageConfigs'));
|
||||
}
|
||||
|
||||
public function underMaintenance(): View
|
||||
{
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
return view('vuexy-admin::pages.under-maintenance', compact('pageConfigs'));
|
||||
}
|
||||
}
|
26
src/Application/Http/Controllers/KonekoModuleController.php
Normal file
26
src/Application/Http/Controllers/KonekoModuleController.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Http\Controllers;
|
||||
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class KonekoModuleController extends Controller
|
||||
{
|
||||
/**
|
||||
* Muestra la vista de configuraciones generales
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function pluginsIndex(): View
|
||||
{
|
||||
return view('vuexy-admin::koneko-vuexy.plugins.index');
|
||||
}
|
||||
|
||||
public function modulesManagementIndex(): View
|
||||
{
|
||||
return view('vuexy-admin::koneko-vuexy.module-management.index');
|
||||
}
|
||||
}
|
26
src/Application/Http/Controllers/LanguageController.php
Normal file
26
src/Application/Http/Controllers/LanguageController.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Http\Controllers;
|
||||
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Http\{Request,RedirectResponse};
|
||||
use Illuminate\Support\Facades\App;
|
||||
|
||||
class LanguageController extends Controller
|
||||
{
|
||||
public function swap(Request $request, $locale): RedirectResponse
|
||||
{
|
||||
if (!in_array($locale, ['es', 'co', 'en', 'fr', 'ar', 'de'])) {
|
||||
abort(400);
|
||||
|
||||
} else {
|
||||
$request->session()->put('locale', $locale);
|
||||
}
|
||||
|
||||
App::setLocale($locale);
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
}
|
29
src/Application/Http/Controllers/PermissionController.php
Normal file
29
src/Application/Http/Controllers/PermissionController.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Http\Controllers;
|
||||
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Http\{Request,JsonResponse};
|
||||
use Illuminate\View\View;
|
||||
use Koneko\VuexyAdmin\Application\UX\ConfigBuilders\Rbac\PermissionsTableConfigBuilder;
|
||||
|
||||
class PermissionController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function index(Request $request): JsonResponse|View
|
||||
{
|
||||
if ($request->ajax()) {
|
||||
$builder = app(PermissionsTableConfigBuilder::class)->getQueryBuilder($request);
|
||||
|
||||
return $builder->getJson();
|
||||
}
|
||||
|
||||
return view('vuexy-admin::settings.rbac.permissions.index');
|
||||
}
|
||||
}
|
41
src/Application/Http/Controllers/RoleController.php
Normal file
41
src/Application/Http/Controllers/RoleController.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Http\Controllers;
|
||||
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\View\View;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
class RoleController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
return view('vuexy-admin::settings.rbac.roles.index');
|
||||
}
|
||||
|
||||
public function checkUniqueRoleName(Request $request): JsonResponse
|
||||
{
|
||||
$id = $request->input('id');
|
||||
$name = $request->input('name');
|
||||
|
||||
// Verificar si el nombre ya existe en la base de datos
|
||||
$existingRole = Role::where('name', $name)
|
||||
->whereNot('id', $id)
|
||||
->first();
|
||||
|
||||
if ($existingRole) {
|
||||
return response()->json(['valid' => false]);
|
||||
}
|
||||
|
||||
return response()->json(['valid' => true]);
|
||||
}
|
||||
}
|
107
src/Application/Http/Controllers/UserController.php
Normal file
107
src/Application/Http/Controllers/UserController.php
Normal file
@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Http\Controllers;
|
||||
|
||||
use Illuminate\Routing\Controller;
|
||||
use Koneko\VuexyAdmin\Models\User;
|
||||
use Koneko\VuexyAdmin\Application\UX\ConfigBuilders\Users\UsersTableConfigBuilder;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Koneko\VuexyAdmin\Application\UI\Avatar\AvatarInitialsService;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AvatarInitialsService $avatarService
|
||||
) {}
|
||||
|
||||
|
||||
/**
|
||||
* Display a listing of the resource (Bootstrap Table AJAX or View).
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function index(Request $request): JsonResponse|View
|
||||
{
|
||||
if ($request->ajax()) {
|
||||
return app(UsersTableConfigBuilder::class)
|
||||
->getQueryBuilder($request)
|
||||
->getJson();
|
||||
}
|
||||
|
||||
return view('vuexy-admin::settings.users.index', ['pageConfigs' => ['contentLayout' => 'wide']]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*
|
||||
* @param int User $user
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function show(User $user): View
|
||||
{
|
||||
return view('vuexy-admin::settings.users.show', compact('user'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the crud for editing the specified resource.
|
||||
*/
|
||||
public function edit(User $user): View
|
||||
{
|
||||
return view('vuexy-admin::settings.users.edit', compact('user'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the crud for editing the specified resource.
|
||||
*/
|
||||
public function delete(User $user): View
|
||||
{
|
||||
return view('vuexy-admin::settings.users.show', compact('user'))->with('mode', 'delete');
|
||||
|
||||
}
|
||||
|
||||
public function generateAvatar(Request $request): BinaryFileResponse
|
||||
{
|
||||
try {
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'color' => 'nullable|string|regex:/^[0-9a-fA-F]{6}$/',
|
||||
'background' => 'nullable|string|regex:/^[0-9a-fA-F]{6}$/',
|
||||
'size' => 'nullable|integer|min:20|max:1024',
|
||||
'max_length' => 'nullable|integer|min:1|max:3'
|
||||
]);
|
||||
|
||||
$response = $this->avatarService->getAvatarImage(
|
||||
name: $validated['name'],
|
||||
color: $validated['color'] ?? null,
|
||||
background: $validated['background'] ?? null,
|
||||
size: $validated['size'] ?? null,
|
||||
maxLength: $validated['max_length'] ?? null
|
||||
);
|
||||
|
||||
$response->headers->set('Cache-Control', 'public, max-age=86400');
|
||||
|
||||
return $response;
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
return $this->avatarService->getAvatarImage(
|
||||
name: 'E R R',
|
||||
color: '#FF0000',
|
||||
maxLength: 3
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function viewerIndex(User $user): View
|
||||
{
|
||||
return view('vuexy-admin::user.viewer.index', compact('user'));
|
||||
}
|
||||
}
|
||||
|
24
src/Application/Http/Controllers/UserProfileController.php
Normal file
24
src/Application/Http/Controllers/UserProfileController.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Http\Controllers;
|
||||
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\View\View;
|
||||
use Koneko\VuexyAdmin\Application\UI\Avatar\AvatarInitialsService;
|
||||
|
||||
class UserProfileController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
return view('vuexy-admin::user.profile.index');
|
||||
}
|
||||
}
|
65
src/Application/Http/Controllers/VuexyNavbarController.php
Normal file
65
src/Application/Http/Controllers/VuexyNavbarController.php
Normal file
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Http\Controllers;
|
||||
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Http\{JsonResponse, Request};
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Koneko\VuexyAdmin\Application\UX\Navbar\{VuexyQuicklinksBuilderService,VuexySearchBarBuilderService};
|
||||
|
||||
class VuexyNavbarController extends Controller
|
||||
{
|
||||
/**
|
||||
* Realiza búsqueda en la barra de navegación
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @throws \Illuminate\Http\Exceptions\HttpResponseException
|
||||
*/
|
||||
public function searchNavbar(): JsonResponse
|
||||
{
|
||||
abort_if(!request()->expectsJson(), 403, __('errors.ajax_only'));
|
||||
|
||||
return response()->json(app(VuexySearchBarBuilderService::class)->getSearchData());
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualiza los enlaces rápidos del usuario
|
||||
*
|
||||
* @param Request $request Datos de la solicitud
|
||||
* @return void
|
||||
* @throws \Illuminate\Http\Exceptions\HttpResponseException
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function quickLinksUpdate(Request $request): void
|
||||
{
|
||||
abort_if(!request()->expectsJson(), 403, __('errors.ajax_only'));
|
||||
|
||||
$validated = $request->validate([
|
||||
'action' => 'required|in:update,remove',
|
||||
'route' => 'required|string',
|
||||
]);
|
||||
|
||||
$key = 'vuexy-quicklinks.user';
|
||||
$userId = Auth::user()->id;
|
||||
|
||||
$quickLinks = settings()->setContext('core', 'navbar')->get($key, $userId)?? [];
|
||||
|
||||
if ($validated['action'] === 'update') {
|
||||
if (!in_array($validated['route'], $quickLinks)) {
|
||||
$quickLinks[] = $validated['route'];
|
||||
}
|
||||
|
||||
} elseif ($validated['action'] === 'remove') {
|
||||
$quickLinks = array_values(array_filter(
|
||||
$quickLinks,
|
||||
fn($route) => $route !== $validated['route']
|
||||
));
|
||||
}
|
||||
|
||||
settings()->setContext('core', 'navbar')->set($key, json_encode($quickLinks), $userId);
|
||||
|
||||
VuexyQuicklinksBuilderService::forgetCacheForUser();
|
||||
}
|
||||
}
|
146
src/Application/Http/Controllers/___AuthUserController.php
Normal file
146
src/Application/Http/Controllers/___AuthUserController.php
Normal file
@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Http\Controllers;
|
||||
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Laravel\Fortify\Features;
|
||||
|
||||
class UsersAuthController extends Controller
|
||||
{
|
||||
/*
|
||||
public function loginView()
|
||||
{
|
||||
dd($viewMode);
|
||||
$viewMode = config('vuexy.custom.authViewMode');
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
return view("vuexy-admin::auth.login-{$viewMode}", ['pageConfigs' => $pageConfigs]);
|
||||
}
|
||||
|
||||
public function registerView()
|
||||
{
|
||||
if (!Features::enabled(Features::registration()))
|
||||
abort(403, 'El registro está deshabilitado.');
|
||||
|
||||
$viewMode = config('vuexy.custom.authViewMode');
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
return view("vuexy-admin::auth.register-{$viewMode}", ['pageConfigs' => $pageConfigs]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function confirmPasswordView()
|
||||
{
|
||||
if (!Features::enabled(Features::registration()))
|
||||
abort(403, 'El registro está deshabilitado.');
|
||||
|
||||
$viewMode = config('vuexy.custom.authViewMode');
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
return view("vuexy-admin::auth.confirm-password-{$viewMode}", ['pageConfigs' => $pageConfigs]);
|
||||
}
|
||||
|
||||
public function resetPasswordView()
|
||||
{
|
||||
if (!Features::enabled(Features::resetPasswords()))
|
||||
abort(403, 'El registro está deshabilitado.');
|
||||
|
||||
$viewMode = config('vuexy.custom.authViewMode');
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
return view("vuexy-admin::auth.reset-password-{$viewMode}", ['pageConfigs' => $pageConfigs]);
|
||||
}
|
||||
|
||||
public function requestPasswordResetLinkView(Request $request)
|
||||
{
|
||||
if (!Features::enabled(Features::resetPasswords()))
|
||||
abort(403, 'El registro está deshabilitado.');
|
||||
|
||||
$viewMode = config('vuexy.custom.authViewMode');
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
return view("vuexy-admin::auth.reset-password-{$viewMode}", ['pageConfigs' => $pageConfigs, 'request' => $request]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public function twoFactorChallengeView()
|
||||
{
|
||||
if (!Features::enabled(Features::registration()))
|
||||
abort(403, 'El registro está deshabilitado.');
|
||||
|
||||
$viewMode = config('vuexy.custom.authViewMode');
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
return view("vuexy-admin::auth.two-factor-challenge-{$viewMode}", ['pageConfigs' => $pageConfigs]);
|
||||
}
|
||||
|
||||
public function twoFactorRecoveryCodesView()
|
||||
{
|
||||
if (!Features::enabled(Features::registration()))
|
||||
abort(403, 'El registro está deshabilitado.');
|
||||
|
||||
$viewMode = config('vuexy.custom.authViewMode');
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
return view("vuexy-admin::auth.register-{$viewMode}", ['pageConfigs' => $pageConfigs]);
|
||||
}
|
||||
|
||||
public function twoFactorAuthenticationView()
|
||||
{
|
||||
if (!Features::enabled(Features::registration()))
|
||||
abort(403, 'El registro está deshabilitado.');
|
||||
|
||||
$viewMode = config('vuexy.custom.authViewMode');
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
return view("vuexy-admin::auth.register-{$viewMode}", ['pageConfigs' => $pageConfigs]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public function verifyEmailView()
|
||||
{
|
||||
if (!Features::enabled(Features::registration()))
|
||||
abort(403, 'El registro está deshabilitado.');
|
||||
|
||||
$viewMode = config('vuexy.custom.authViewMode');
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
return view("vuexy-admin::auth.verify-email-{$viewMode}", ['pageConfigs' => $pageConfigs]);
|
||||
}
|
||||
|
||||
public function showEmailVerificationForm()
|
||||
{
|
||||
if (!Features::enabled(Features::registration()))
|
||||
abort(403, 'El registro está deshabilitado.');
|
||||
|
||||
$viewMode = config('vuexy.custom.authViewMode');
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
return view("vuexy-admin::auth.register-{$viewMode}", ['pageConfigs' => $pageConfigs]);
|
||||
}
|
||||
|
||||
public function userProfileView()
|
||||
{
|
||||
if (!Features::enabled(Features::registration()))
|
||||
abort(403, 'El registro está deshabilitado.');
|
||||
|
||||
$viewMode = config('vuexy.custom.authViewMode');
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
return view("vuexy-admin::auth.register-{$viewMode}", ['pageConfigs' => $pageConfigs]);
|
||||
}
|
||||
*/
|
||||
}
|
190
src/Application/Http/Controllers/___VuexyController.php
Normal file
190
src/Application/Http/Controllers/___VuexyController.php
Normal file
@ -0,0 +1,190 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Http\Controllers;
|
||||
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\View;
|
||||
use Koneko\VuexyAdmin\Application\UX\Navbar\{VuexyQuicklinksBuilderService,VuexySearchBuilderService};
|
||||
use Koneko\VuexyAdmin\Application\UI\Avatar\AvatarInitialsService;
|
||||
use Koneko\VuexyAdmin\Application\UX\ConfigBuilders\System\EnvironmentVarsTableConfigBuilder;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
|
||||
/**
|
||||
* Controlador para la gestión de funcionalidades administrativas de Vuexy
|
||||
*/
|
||||
class VuexyController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AvatarInitialsService $avatarService
|
||||
) {}
|
||||
|
||||
|
||||
/**
|
||||
* Realiza búsqueda en la barra de navegación
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @throws \Illuminate\Http\Exceptions\HttpResponseException
|
||||
*/
|
||||
public function searchNavbar(): JsonResponse
|
||||
{
|
||||
abort_if(!request()->expectsJson(), 403, __('errors.ajax_only'));
|
||||
|
||||
return response()->json(app(VuexySearchBuilderService::class)->getForUser());
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualiza los enlaces rápidos del usuario
|
||||
*
|
||||
* @param Request $request Datos de la solicitud
|
||||
* @return void
|
||||
* @throws \Illuminate\Http\Exceptions\HttpResponseException
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function quickLinksUpdate(Request $request): void
|
||||
{
|
||||
abort_if(!request()->expectsJson(), 403, __('errors.ajax_only'));
|
||||
|
||||
$validated = $request->validate([
|
||||
'action' => 'required|in:update,remove',
|
||||
'route' => 'required|string',
|
||||
]);
|
||||
|
||||
$quickLinks = settings()->get('quicklinks', Auth::user()->id, []);
|
||||
|
||||
if ($validated['action'] === 'update') {
|
||||
if (!in_array($validated['route'], $quickLinks)) {
|
||||
$quickLinks[] = $validated['route'];
|
||||
}
|
||||
} elseif ($validated['action'] === 'remove') {
|
||||
$quickLinks = array_values(array_filter(
|
||||
$quickLinks,
|
||||
fn($route) => $route !== $validated['route']
|
||||
));
|
||||
}
|
||||
|
||||
settings()->set('quicklinks', json_encode($quickLinks), Auth::user()->id, 'vuexy-admin');
|
||||
|
||||
app(VuexyQuicklinksBuilderService::class)->clearCache(Auth::user());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Muestra la vista de configuraciones generales
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function generalSettings(): View
|
||||
{
|
||||
return view('vuexy-admin::general-settings.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Muestra la vista de configuraciones SMTP
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function smtpSettings(): View
|
||||
{
|
||||
return view('vuexy-admin::smtp-settings.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Muestra la vista de configuraciones de interfaz
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function InterfaceSettings(): View
|
||||
{
|
||||
return view('vuexy-admin::interface-settings.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Muestra el listado de accesos al sistema (Bootstrap Table AJAX or View).
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse|\Illuminate\View\View
|
||||
*/
|
||||
public function userLogs(Request $request): JsonResponse|View
|
||||
{
|
||||
if ($request->ajax()) {
|
||||
$builder = app(UserLoginTableConfigBuilder::class)->getQueryBuilder($request);
|
||||
|
||||
return $builder->getJson();
|
||||
}
|
||||
|
||||
return view('vuexy-admin::user-logs.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Muestra el listado de eventos de auditoría (Bootstrap Table AJAX or View).
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse|\Illuminate\View\View
|
||||
*/
|
||||
public function securityEvents(Request $request): JsonResponse|View
|
||||
{
|
||||
if ($request->ajax()) {
|
||||
$builder = app(SecurityEventsTableConfigBuilder::class)->getQueryBuilder($request);
|
||||
|
||||
return $builder->getJson();
|
||||
}
|
||||
|
||||
return view('vuexy-admin::security-events.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a listing of the resource (Bootstrap Table AJAX or View).
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function environmentVars(Request $request): JsonResponse|View
|
||||
{
|
||||
if ($request->ajax()) {
|
||||
$builder = app(EnvironmentVarsTableConfigBuilder::class)->getQueryBuilder($request);
|
||||
|
||||
return $builder->getJson();
|
||||
}
|
||||
|
||||
return view('vuexy-admin::environment-vars.index');
|
||||
}
|
||||
|
||||
public function generateAvatar(Request $request): BinaryFileResponse
|
||||
{
|
||||
try {
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'color' => 'nullable|string|regex:/^[0-9a-fA-F]{6}$/',
|
||||
'background' => 'nullable|string|regex:/^[0-9a-fA-F]{6}$/',
|
||||
'size' => 'nullable|integer|min:20|max:1024',
|
||||
'max_length' => 'nullable|integer|min:1|max:3'
|
||||
]);
|
||||
|
||||
$response = $this->avatarService->getAvatarImage(
|
||||
name: $validated['name'],
|
||||
forcedColor: $validated['color'] ?? null,
|
||||
forcedBackground: $validated['background'] ?? null,
|
||||
size: $validated['size'] ?? null,
|
||||
maxLength: $validated['max_length'] ?? null
|
||||
);
|
||||
|
||||
$response->headers->set('Cache-Control', 'public, max-age=86400');
|
||||
|
||||
return $response;
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
return $this->avatarService->getAvatarImage(
|
||||
name: 'E R R',
|
||||
forcedColor: '#FF0000',
|
||||
maxLength: 3
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
38
src/Application/Http/Middleware/AdminTemplateMiddleware.php
Normal file
38
src/Application/Http/Middleware/AdminTemplateMiddleware.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Koneko\VuexyAdmin\Application\Cache\VuexyVarsBuilderService;
|
||||
use Koneko\VuexyAdmin\Application\UX\Content\VuexyBreadcrumbsBuilderService;
|
||||
use Koneko\VuexyAdmin\Application\UX\Menu\VuexyMenuFormatter;
|
||||
use Koneko\VuexyAdmin\Application\UX\Notifications\VuexyNotificationsBuilderService;
|
||||
use Koneko\VuexyAdmin\Application\UX\Template\{VuexyConfigSynchronizer};
|
||||
|
||||
class AdminTemplateMiddleware
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
// Aplicar configuración de layout antes de que la vista se cargue
|
||||
if (str_contains($request->header('Accept'), 'text/html')) {
|
||||
app(VuexyConfigSynchronizer::class)->sync();
|
||||
|
||||
View::share([
|
||||
'_admin' => app(VuexyVarsBuilderService::class)->getAdminVars(),
|
||||
'vuexyMenu' => app(VuexyMenuFormatter::class)->getMenu(),
|
||||
'vuexyNotifications' => app(VuexyNotificationsBuilderService::class)->getForUser(),
|
||||
'vuexyBreadcrumbs' => app(VuexyBreadcrumbsBuilderService::class)->getBreadcrumbs(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
25
src/Application/Http/Middleware/LocaleMiddleware.php
Normal file
25
src/Application/Http/Middleware/LocaleMiddleware.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class LocaleMiddleware
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// Locale is enabled and allowed to be change
|
||||
if (session()->has('locale') && in_array(session()->get('locale'), ['es', 'co', 'en', 'fr', 'ar', 'de'])) {
|
||||
app()->setLocale(session()->get('locale'));
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\{Auth,Session};
|
||||
use Koneko\VuexyAdmin\Models\UserLogin;
|
||||
|
||||
class TrackSessionActivity
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if (!Auth::check() && Session::has('user_last_login_id')) {
|
||||
$this->closeExpiredSession();
|
||||
}
|
||||
|
||||
if (Auth::check()) {
|
||||
$this->handleIdleTimeout($request); // antes de actualizar la actividad
|
||||
$this->updateLastActivity();
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cierra la sesión de usuario marcada como activa cuando detectamos expiración.
|
||||
*/
|
||||
private function closeExpiredSession(): void
|
||||
{
|
||||
$lastLoginId = Session::pull('user_last_login_id'); // pull: obtiene y elimina al mismo tiempo
|
||||
|
||||
if (!$lastLoginId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$userLogin = UserLogin::where('id', $lastLoginId)
|
||||
->whereNull('logout_at')
|
||||
->first();
|
||||
|
||||
if ($userLogin) {
|
||||
$userLogin->update([
|
||||
'logout_at' => now(),
|
||||
'logout_reason' => 'session_expired',
|
||||
]);
|
||||
|
||||
logger()->info("[TrackSessionActivity] ✅ Logout automático registrado para login_id: {$lastLoginId}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Guarda el timestamp actual como última actividad.
|
||||
*/
|
||||
private function updateLastActivity(): void
|
||||
{
|
||||
Session::put('last_activity_at', now());
|
||||
}
|
||||
|
||||
private function handleIdleTimeout(Request $request): void
|
||||
{
|
||||
$timeoutMinutes = config('session.idle_timeout', 30); // puedes agregar en tu config/session.php
|
||||
$lastActivity = Session::get('last_activity_at');
|
||||
|
||||
if ($lastActivity && now()->diffInMinutes($lastActivity) >= $timeoutMinutes) {
|
||||
$this->forceLogoutDueToIdle();
|
||||
}
|
||||
}
|
||||
|
||||
private function forceLogoutDueToIdle(): void
|
||||
{
|
||||
$lastLoginId = Session::pull('user_last_login_id');
|
||||
|
||||
if ($lastLoginId) {
|
||||
$userLogin = UserLogin::where('id', $lastLoginId)
|
||||
->whereNull('logout_at')
|
||||
->first();
|
||||
|
||||
if ($userLogin) {
|
||||
$userLogin->update([
|
||||
'logout_at' => now(),
|
||||
'logout_reason' => 'idle_timeout',
|
||||
]);
|
||||
|
||||
logger()->info("[TrackSessionActivity] ⏳ Sesión cerrada por inactividad para login_id: {$lastLoginId}");
|
||||
}
|
||||
}
|
||||
|
||||
Auth::logout(); // Use Auth facade to logout
|
||||
Session::invalidate(); // limpia toda la sesión
|
||||
}
|
||||
}
|
51
src/Application/Jobs/Security/RotateVaultKeysJob.php
Normal file
51
src/Application/Jobs/Security/RotateVaultKeysJob.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Jobs\Security;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\{InteractsWithQueue, SerializesModels};
|
||||
use Koneko\VuexyAdmin\Application\Security\VaultKeyService;
|
||||
use Koneko\VuexyAdmin\Models\VaultKey;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class RotateVaultKeysJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected ?int $rotationThresholdDays;
|
||||
|
||||
public function __construct(?int $rotationThresholdDays = null)
|
||||
{
|
||||
$this->rotationThresholdDays = $rotationThresholdDays;
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$query = VaultKey::query()->where('is_active', true);
|
||||
|
||||
if ($this->rotationThresholdDays) {
|
||||
$query->where('rotated_at', '<=', now()->subDays($this->rotationThresholdDays));
|
||||
}
|
||||
|
||||
$keysToRotate = $query->get();
|
||||
|
||||
if ($keysToRotate->isEmpty()) {
|
||||
Log::info('🔄 No hay claves que requieran rotación en este ciclo.');
|
||||
return;
|
||||
}
|
||||
|
||||
$service = app(VaultKeyService::class);
|
||||
|
||||
foreach ($keysToRotate as $key) {
|
||||
try {
|
||||
$service->rotateKey($key->alias);
|
||||
Log::info("✅ Clave '{$key->alias}' rotada correctamente.");
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
Log::error("❌ Error al rotar la clave '{$key->alias}': " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
36
src/Application/Jobs/Users/ForceLogoutInactiveUsersJob.php
Normal file
36
src/Application/Jobs/Users/ForceLogoutInactiveUsersJob.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Jobs\Users;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Koneko\VuexyAdmin\Models\UserLogin;
|
||||
|
||||
class ForceLogoutInactiveUsersJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, Queueable;
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$timeoutMinutes = config('session.idle_timeout', 30);
|
||||
|
||||
$cutoff = now()->subMinutes($timeoutMinutes);
|
||||
|
||||
$sessions = UserLogin::whereNull('logout_at')
|
||||
->where('created_at', '<=', $cutoff)
|
||||
->get();
|
||||
|
||||
foreach ($sessions as $session) {
|
||||
$session->update([
|
||||
'logout_at' => now(),
|
||||
'logout_reason' => 'forced_logout_by_job',
|
||||
]);
|
||||
|
||||
Log::info("[ForceLogoutInactiveUsersJob] 🔒 Sesión cerrada por inactividad. UserLogin ID: {$session->id}");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Listeners\Authentication;
|
||||
|
||||
use Illuminate\Auth\Events\Failed;
|
||||
use Koneko\VuexyAdmin\Application\Logger\KonekoSecurityAuditLogger;
|
||||
|
||||
class HandleFailedLogin
|
||||
{
|
||||
protected $auditService;
|
||||
|
||||
public function __construct(KonekoSecurityAuditLogger $auditService)
|
||||
{
|
||||
$this->auditService = $auditService;
|
||||
}
|
||||
|
||||
public function handle(Failed $event)
|
||||
{
|
||||
$request = request();
|
||||
|
||||
$this->auditService->logEvent('failed_login_attempt', $request, $event->user?->id, [
|
||||
'credentials' => $event->credentials,
|
||||
]);
|
||||
}
|
||||
}
|
109
src/Application/Listeners/Authentication/HandleUserLogin.php
Normal file
109
src/Application/Listeners/Authentication/HandleUserLogin.php
Normal file
@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Listeners\Authentication;
|
||||
|
||||
use Illuminate\Auth\Events\Login;
|
||||
use Jenssegers\Agent\Agent;
|
||||
use GeoIp2\Database\Reader;
|
||||
use Koneko\VuexyAdmin\Models\UserLogin;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class HandleUserLogin
|
||||
{
|
||||
public function handle(Login $event): void
|
||||
{
|
||||
$request = request();
|
||||
|
||||
// Extraer información avanzada
|
||||
$loginData = $this->collectLoginData($event, $request);
|
||||
|
||||
// Registrar Login
|
||||
UserLogin::create($loginData);
|
||||
|
||||
// Actualizar información de último login en usuario
|
||||
$event->user->update([
|
||||
'last_login_at' => now(),
|
||||
'last_login_ip' => $request->ip(),
|
||||
]);
|
||||
|
||||
// Opcional: Enviar notificación por email
|
||||
//$this->notifyUser($event->user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrae y prepara datos para el registro de login.
|
||||
*/
|
||||
protected function collectLoginData(Login $event, $request): array
|
||||
{
|
||||
$agent = new Agent();
|
||||
$ip = $request->ip();
|
||||
|
||||
// Información básica
|
||||
$data = [
|
||||
'user_id' => $event->user->id,
|
||||
'ip_address' => $ip,
|
||||
'user_agent' => $request->userAgent(),
|
||||
'login_success' => true, // Asumimos que al llegar aquí es exitoso
|
||||
];
|
||||
|
||||
// Información del dispositivo
|
||||
$data['device_type'] = $agent->device();
|
||||
$data['browser'] = $agent->browser();
|
||||
$data['browser_version'] = $agent->version($agent->browser());
|
||||
$data['os'] = $agent->platform();
|
||||
$data['os_version'] = $agent->version($agent->platform());
|
||||
|
||||
// Información de ubicación
|
||||
try {
|
||||
$geoData = $this->getGeoIpData($ip);
|
||||
$data = array_merge($data, $geoData);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::warning("No se pudo obtener GeoIP para IP: {$ip} - {$e->getMessage()}");
|
||||
}
|
||||
|
||||
// Información adicional (headers, etc.)
|
||||
$data['additional_info'] = json_encode([
|
||||
'headers' => $request->headers->all(),
|
||||
]);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene datos de geolocalización usando MaxMind GeoIP2.
|
||||
*/
|
||||
protected function getGeoIpData(string $ip): array
|
||||
{
|
||||
$reader = new Reader(public_path('vendor/geoip/GeoLite2-City.mmdb'));
|
||||
$record = $reader->city($ip);
|
||||
|
||||
return [
|
||||
'country' => $record->country->name ?? null,
|
||||
'region' => $record->mostSpecificSubdivision->name ?? null,
|
||||
'city' => $record->city->name ?? null,
|
||||
'lat' => $record->location->latitude ?? null,
|
||||
'lng' => $record->location->longitude ?? null,
|
||||
'is_proxy' => $this->detectProxy($ip),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Detecta si una IP es Proxy/VPN mediante integración externa (opcional).
|
||||
*/
|
||||
protected function detectProxy(string $ip): bool
|
||||
{
|
||||
// TODO: Integrar API externa para detección de Proxy/VPN.
|
||||
return false; // Por defecto, asumimos no proxy.
|
||||
}
|
||||
|
||||
/**
|
||||
* Enviar notificación por correo al usuario (opcional).
|
||||
*/
|
||||
protected function notifyUser($user): void
|
||||
{
|
||||
//Mail::to($user->email)->send(new LoginNotification($user));
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Listeners\Authentication;
|
||||
|
||||
use Illuminate\Auth\Events\Logout;
|
||||
use Koneko\VuexyAdmin\Application\UX\Navbar\{VuexySearchBarBuilderService,VuexyQuicklinksBuilderService};
|
||||
use Koneko\VuexyAdmin\Application\UX\Menu\VuexyMenuFormatter;
|
||||
use Koneko\VuexyAdmin\Application\UX\Notifications\VuexyNotificationsBuilderService;
|
||||
use Koneko\VuexyAdmin\Models\UserLogin;
|
||||
|
||||
class HandleUserLogout
|
||||
{
|
||||
public function handle(Logout $event)
|
||||
{
|
||||
if ($event->user) {
|
||||
$userId = $event->user->id;
|
||||
|
||||
// Actualiza el registro de login más reciente que no tenga logout registrado
|
||||
UserLogin::closeLastActiveLoginForUser($userId);
|
||||
|
||||
// Limpia la cache de entorno de usuario
|
||||
$this->clearUserCaches($userId);
|
||||
}
|
||||
}
|
||||
|
||||
private function clearUserCaches(int $userId): void
|
||||
{
|
||||
VuexyMenuFormatter::forgetCacheForUser($userId);
|
||||
VuexySearchBarBuilderService::forgetCacheForUser($userId);
|
||||
VuexyQuicklinksBuilderService::clearCacheForUser($userId);
|
||||
VuexyNotificationsBuilderService::clearCacheForUser($userId);
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Listeners\Settings;
|
||||
|
||||
use Koneko\VuexyAdmin\Application\Events\Settings\VuexyCustomizerSettingsUpdated;
|
||||
use Koneko\VuexyAdmin\Application\Cache\VuexyVarsBuilderService;
|
||||
|
||||
class ApplyVuexyCustomizerSettings
|
||||
{
|
||||
public function handle(VuexyCustomizerSettingsUpdated $event): void
|
||||
{
|
||||
foreach ($event->settings as $key => $value) {
|
||||
settings()->self('vuexy')->set($key, $value);
|
||||
}
|
||||
|
||||
VuexyVarsBuilderService::clearCache();
|
||||
}
|
||||
}
|
23
src/Application/Listeners/Settings/SettingCacheListener.php
Normal file
23
src/Application/Listeners/Settings/SettingCacheListener.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Listeners\Settings;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Koneko\VuexyAdmin\Application\Events\Settings\SettingChanged;
|
||||
|
||||
class SettingCacheListener
|
||||
{
|
||||
public function handle(SettingChanged $event): void
|
||||
{
|
||||
$keyHash = md5($event->key);
|
||||
/*
|
||||
$groupHash = md5($event->namespace);
|
||||
$suffix = $event->userId !== null ? "u:{$event->userId}" : 'global';
|
||||
|
||||
Cache::forget("koneko.admin.settings.setting:{$keyHash}:{$suffix}");
|
||||
Cache::forget("koneko.admin.settings.group:{$groupHash}:{$suffix}");
|
||||
*/
|
||||
}
|
||||
}
|
70
src/Application/Logger/KonekoSecurityAuditLogger.php
Normal file
70
src/Application/Logger/KonekoSecurityAuditLogger.php
Normal file
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Logger;
|
||||
|
||||
use GeoIp2\Database\Reader;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Jenssegers\Agent\Agent;
|
||||
use Koneko\VuexyAdmin\Models\SecurityEvent;
|
||||
|
||||
class KonekoSecurityAuditLogger
|
||||
{
|
||||
/**
|
||||
* Registra un nuevo evento de seguridad.
|
||||
*/
|
||||
public function logEvent(string $type, Request $request, ?int $userId = null, array $payload = [], bool $isProxy = false)
|
||||
{
|
||||
$agent = new Agent();
|
||||
$ip = $request->ip();
|
||||
|
||||
$geoData = $this->getGeoData($ip);
|
||||
|
||||
SecurityEvent::create([
|
||||
'user_id' => $userId,
|
||||
'event_type' => $type,
|
||||
'status' => 'new',
|
||||
'ip_address' => $ip,
|
||||
'user_agent' => $request->userAgent(),
|
||||
'device_type' => $agent->device(),
|
||||
'browser' => $agent->browser(),
|
||||
'browser_version' => $agent->version($agent->browser()),
|
||||
'os' => $agent->platform(),
|
||||
'os_version' => $agent->version($agent->platform()),
|
||||
'country' => $geoData['country'] ?? null,
|
||||
'region' => $geoData['region'] ?? null,
|
||||
'city' => $geoData['city'] ?? null,
|
||||
'lat' => $geoData['latitude'] ?? null,
|
||||
'lng' => $geoData['longitude'] ?? null,
|
||||
'is_proxy' => $isProxy,
|
||||
'url' => $request->fullUrl(),
|
||||
'http_method' => $request->method(),
|
||||
'payload' => json_encode($payload),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene datos de geolocalización usando MaxMind GeoIP2.
|
||||
*/
|
||||
private function getGeoData(string $ip): array
|
||||
{
|
||||
try {
|
||||
$reader = new Reader(public_path('vendor/geoip/GeoLite2-City.mmdb'));
|
||||
$record = $reader->city($ip);
|
||||
|
||||
return [
|
||||
'country' => $record->country->name,
|
||||
'region' => $record->mostSpecificSubdivision->name,
|
||||
'city' => $record->city->name,
|
||||
'latitude' => $record->location->latitude,
|
||||
'longitude' => $record->location->longitude,
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::warning("GeoIP falló para IP: {$ip} - {$e->getMessage()}");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
51
src/Application/Logger/KonekoSecurityLogger.php
Normal file
51
src/Application/Logger/KonekoSecurityLogger.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Support\Logger;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Jenssegers\Agent\Agent;
|
||||
use Koneko\VuexyAdmin\Application\Enums\SecurityEvents\SecurityEventStatus;
|
||||
use Koneko\VuexyAdmin\Application\Enums\SecurityEvents\SecurityEventType;
|
||||
use Koneko\VuexyAdmin\Application\Bootstrap\KonekoComponentContextRegistrar;
|
||||
use Koneko\VuexyAdmin\Models\SecurityEvent;
|
||||
use Koneko\VuexyAdmin\Support\Geo\GeoLocationResolver;
|
||||
|
||||
class KonekoSecurityLogger
|
||||
{
|
||||
public static function log(
|
||||
SecurityEventType|string $type,
|
||||
?Request $request = null,
|
||||
?int $userId = null,
|
||||
array $payload = [],
|
||||
bool $isProxy = false
|
||||
): ?SecurityEvent {
|
||||
$request ??= request();
|
||||
|
||||
$agent = new Agent(null, $request->userAgent());
|
||||
$geo = GeoLocationResolver::resolve($request->ip());
|
||||
|
||||
return SecurityEvent::create([
|
||||
'module' => KonekoComponentContextRegistrar::currentComponent() ?? 'unknown',
|
||||
'user_id' => $userId ?? Auth::id(),
|
||||
'event_type' => (string) $type,
|
||||
'status' => SecurityEventStatus::NEW,
|
||||
'ip_address' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
'device_type' => $agent->device() ?: ($geo['device_type'] ?? null),
|
||||
'browser' => $agent->browser() ?: ($geo['browser'] ?? null),
|
||||
'browser_version' => $agent->version($agent->browser()) ?: ($geo['browser_version'] ?? null),
|
||||
'os' => $agent->platform() ?: ($geo['os'] ?? null),
|
||||
'os_version' => $agent->version($agent->platform()) ?: ($geo['os_version'] ?? null),
|
||||
'country' => $geo['country'] ?? null,
|
||||
'region' => $geo['region'] ?? null,
|
||||
'city' => $geo['city'] ?? null,
|
||||
'lat' => $geo['lat'] ?? null,
|
||||
'lng' => $geo['lng'] ?? null,
|
||||
'is_proxy' => $isProxy,
|
||||
'url' => $request->fullUrl(),
|
||||
'http_method' => $request->method(),
|
||||
'payload' => $payload,
|
||||
]);
|
||||
}
|
||||
}
|
73
src/Application/Logger/KonekoSystemLogger.php
Normal file
73
src/Application/Logger/KonekoSystemLogger.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Alication\Logger;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\Request;
|
||||
use Koneko\VuexyAdmin\Application\Enums\SystemLog\{LogTriggerType, LogLevel};
|
||||
use Koneko\VuexyAdmin\Application\Bootstrap\KonekoComponentContextRegistrar;
|
||||
use Koneko\VuexyAdmin\Models\SystemLog;
|
||||
|
||||
/**
|
||||
* ✨ Logger de sistema contextual para el ecosistema Koneko
|
||||
*/
|
||||
class KonekoSystemLogger
|
||||
{
|
||||
protected string $component;
|
||||
|
||||
public function __construct(?string $component = null)
|
||||
{
|
||||
$this->component = $component ?? KonekoComponentContextRegistrar::currentComponent() ?? 'core';
|
||||
}
|
||||
|
||||
public function log(
|
||||
LogLevel|string $level,
|
||||
string $message,
|
||||
array $context = [],
|
||||
LogTriggerType|string $triggerType = LogTriggerType::System,
|
||||
?int $triggerId = null,
|
||||
?Model $relatedModel = null
|
||||
): SystemLog {
|
||||
return SystemLog::create([
|
||||
'module' => $this->component,
|
||||
'user_id' => auth()->id(),
|
||||
'level' => $level instanceof LogLevel ? $level->value : $level,
|
||||
'message' => $message,
|
||||
'context' => $context,
|
||||
'trigger_type' => $triggerType instanceof LogTriggerType ? $triggerType->value : $triggerType,
|
||||
'trigger_id' => $triggerId,
|
||||
'loggable_id' => $relatedModel?->getKey(),
|
||||
'loggable_type'=> $relatedModel?->getMorphClass(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function info(string $message, array $context = []): SystemLog
|
||||
{
|
||||
return $this->log(LogLevel::Info, $message, $context);
|
||||
}
|
||||
|
||||
public function warning(string $message, array $context = []): SystemLog
|
||||
{
|
||||
return $this->log(LogLevel::Warning, $message, $context);
|
||||
}
|
||||
|
||||
public function error(string $message, array $context = []): SystemLog
|
||||
{
|
||||
return $this->log(LogLevel::Error, $message, $context);
|
||||
}
|
||||
|
||||
public function debug(string $message, array $context = []): SystemLog
|
||||
{
|
||||
return $this->log(LogLevel::Debug, $message, $context);
|
||||
}
|
||||
|
||||
public function withTrigger(LogTriggerType|string $type, ?int $id = null): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->triggerType = $type;
|
||||
$clone->triggerId = $id;
|
||||
return $clone;
|
||||
}
|
||||
}
|
47
src/Application/Logger/KonekoUserInteractionLogger.php
Normal file
47
src/Application/Logger/KonekoUserInteractionLogger.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Logger;
|
||||
|
||||
use Illuminate\Support\Facades\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Koneko\VuexyAdmin\Application\Enums\UserInteractions\InteractionSecurityLevel;
|
||||
use Koneko\VuexyAdmin\Application\Bootstrap\KonekoComponentContextRegistrar;
|
||||
use Koneko\VuexyAdmin\Models\UserInteraction;
|
||||
use Koneko\VuexyAdmin\Models\User;
|
||||
|
||||
/**
|
||||
* 📋 Logger de interacciones de usuario con nivel de seguridad.
|
||||
*/
|
||||
class KonekoUserInteractionLogger
|
||||
{
|
||||
public static function record(
|
||||
string $action,
|
||||
array $context = [],
|
||||
InteractionSecurityLevel|string $security = 'normal',
|
||||
?string $livewireComponent = null,
|
||||
?int $userId = null
|
||||
): ?UserInteraction {
|
||||
$userId = $userId ?? Auth::id();
|
||||
if (!$userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
$user = Auth::user();
|
||||
|
||||
return UserInteraction::create([
|
||||
'module' => KonekoComponentContextRegistrar::currentComponent(),
|
||||
'user_id' => $userId,
|
||||
'action' => $action,
|
||||
'livewire_component'=> $livewireComponent,
|
||||
'security_level' => is_string($security) ? InteractionSecurityLevel::from($security) : $security,
|
||||
'ip_address' => Request::ip(),
|
||||
'user_agent' => Request::userAgent(),
|
||||
'context' => $context,
|
||||
'user_flags' => $user->flags_array ?? [],
|
||||
'user_roles' => $user->getRoleNames()->toArray(),
|
||||
]);
|
||||
}
|
||||
}
|
82
src/Application/Logger/____SystemLoggerService.php
Normal file
82
src/Application/Logger/____SystemLoggerService.php
Normal file
@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Services;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Koneko\VuexyAdmin\Models\SystemLog;
|
||||
use Koneko\VuexyAdmin\Application\Enums\{LogLevel, LogTriggerType};
|
||||
|
||||
class ____SystemLoggerService
|
||||
{
|
||||
/**
|
||||
* Registra un log en la tabla system_logs.
|
||||
*/
|
||||
public function log(
|
||||
string|LogLevel $level,
|
||||
string $module,
|
||||
string $message,
|
||||
array $context = [],
|
||||
LogTriggerType $triggerType = LogTriggerType::System,
|
||||
?int $triggerId = null,
|
||||
?Model $relatedModel = null
|
||||
): SystemLog {
|
||||
return SystemLog::create([
|
||||
'module' => $module,
|
||||
'level' => $level instanceof LogLevel ? $level : LogLevel::from($level),
|
||||
'message' => $message,
|
||||
'context' => $context,
|
||||
'trigger_type' => $triggerType,
|
||||
'trigger_id' => $triggerId,
|
||||
'user_id' => Auth::id(),
|
||||
'related_model_type' => $relatedModel?->getMorphClass(),
|
||||
'related_model_id' => $relatedModel?->getKey(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function info(
|
||||
string $module,
|
||||
string $message,
|
||||
array $context = [],
|
||||
LogTriggerType $triggerType = LogTriggerType::System,
|
||||
?int $triggerId = null,
|
||||
?Model $relatedModel = null
|
||||
): SystemLog {
|
||||
return $this->log(LogLevel::Info, $module, $message, $context, $triggerType, $triggerId, $relatedModel);
|
||||
}
|
||||
|
||||
public function warning(
|
||||
string $module,
|
||||
string $message,
|
||||
array $context = [],
|
||||
LogTriggerType $triggerType = LogTriggerType::System,
|
||||
?int $triggerId = null,
|
||||
?Model $relatedModel = null
|
||||
): SystemLog {
|
||||
return $this->log(LogLevel::Warning, $module, $message, $context, $triggerType, $triggerId, $relatedModel);
|
||||
}
|
||||
|
||||
public function error(
|
||||
string $module,
|
||||
string $message,
|
||||
array $context = [],
|
||||
LogTriggerType $triggerType = LogTriggerType::System,
|
||||
?int $triggerId = null,
|
||||
?Model $relatedModel = null
|
||||
): SystemLog {
|
||||
return $this->log(LogLevel::Error, $module, $message, $context, $triggerType, $triggerId, $relatedModel);
|
||||
}
|
||||
|
||||
public function debug(
|
||||
string $module,
|
||||
string $message,
|
||||
array $context = [],
|
||||
LogTriggerType $triggerType = LogTriggerType::System,
|
||||
?int $triggerId = null,
|
||||
?Model $relatedModel = null
|
||||
): SystemLog {
|
||||
return $this->log(LogLevel::Debug, $module, $message, $context, $triggerType, $triggerId, $relatedModel);
|
||||
}
|
||||
}
|
68
src/Application/Logger/____UserInteractionLoggerService.php
Normal file
68
src/Application/Logger/____UserInteractionLoggerService.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Logger;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Request;
|
||||
use Koneko\VuexyAdmin\Models\UserInteraction;
|
||||
use Koneko\VuexyAdmin\Application\Enums\UserInteractions\InteractionSecurityLevel;
|
||||
use Koneko\VuexyAdmin\Application\Bootstrap\KonekoModuleRegistry;
|
||||
|
||||
class ____UserInteractionLoggerService
|
||||
{
|
||||
public function record(
|
||||
string $action,
|
||||
array $context = [],
|
||||
InteractionSecurityLevel|string $security = InteractionSecurityLevel::Normal,
|
||||
?string $livewireComponent = null
|
||||
): ?UserInteraction {
|
||||
$user = Auth::user();
|
||||
|
||||
if (!$user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$component = $this->resolveCurrentComponent();
|
||||
|
||||
return UserInteraction::create([
|
||||
'user_id' => $user->id,
|
||||
'actor_type' => $user->actor_type ?? 'unknown',
|
||||
'component' => $component,
|
||||
'livewire_component' => $livewireComponent,
|
||||
'action' => $action,
|
||||
'security_level' => is_string($security) ? $security : $security->value,
|
||||
'ip_address' => Request::ip(),
|
||||
'user_agent' => Request::userAgent(),
|
||||
'context' => $context,
|
||||
'user_flags' => $user->flags ?? [],
|
||||
'user_roles' => $user->roles ?? [],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Infiera el componente actual (el módulo) para la auditoría.
|
||||
*/
|
||||
private function resolveCurrentComponent(): string
|
||||
{
|
||||
// Opcional: Puedes mejorarlo si quieres tracking de "módulo activo"
|
||||
$modules = KonekoModuleRegistry::enabled();
|
||||
|
||||
if (empty($modules)) {
|
||||
return 'unknown-module';
|
||||
}
|
||||
|
||||
// Si hay muchos módulos activos, podrías basarlo en la ruta actual
|
||||
$currentRoute = request()->route()?->getName();
|
||||
|
||||
foreach ($modules as $module) {
|
||||
if (str_contains($currentRoute, $module->slug)) {
|
||||
return $module->getId(); // Este es slugificado
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: solo devuelve el primero activo
|
||||
return reset($modules)?->getId() ?? 'unknown-module';
|
||||
}
|
||||
}
|
70
src/Application/Macros/StrMacros.php
Normal file
70
src/Application/Macros/StrMacros.php
Normal file
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Macros;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class StrMacros
|
||||
{
|
||||
public static function register(): void
|
||||
{
|
||||
// Macro: pluralEs → Pluraliza una sola palabra en español
|
||||
if (! Str::hasMacro('pluralEs')) {
|
||||
Str::macro('pluralEs', function (string $word): string {
|
||||
$word = trim(Str::lower($word));
|
||||
|
||||
return match (true) {
|
||||
Str::endsWith($word, ['í', 'ú']) => $word . 'es',
|
||||
Str::endsWith($word, 'z') => Str::replaceLast('z', 'ces', $word),
|
||||
Str::endsWith($word, ['s', 'x']) => $word,
|
||||
Str::endsWith($word, ['á', 'é', 'ó']) => $word . 'es',
|
||||
Str::endsWith($word, ['a', 'e', 'o']) => $word . 's',
|
||||
default => $word . 'es',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Macro: pluralEsPhrase → Pluraliza solo la primera palabra de una frase
|
||||
if (! Str::hasMacro('pluralEsPhrase')) {
|
||||
Str::macro('pluralEsPhrase', function (string $phrase): string {
|
||||
$phrase = trim($phrase);
|
||||
|
||||
if (empty($phrase)) return $phrase;
|
||||
|
||||
$exceptions = [
|
||||
'admin', 'catalogo', 'cbb', 'cce', 'cfdi', 'clave', 'crm', 'csv', 'curp', 'diot', 'domicilio',
|
||||
'email', 'erp', 'folio', 'gps', 'gsm', 'hardware', 'ine', 'internet', 'iep', 'isr', 'iva',
|
||||
'json', 'licencia', 'log', 'modulo', 'nss', 'pdf', 'poliza', 'registro', 'retencion', 'rol',
|
||||
'root', 'rfc', 'sat', 'saldo', 'sku', 'software', 'stock', 'subsidio', 'timbrado', 'token',
|
||||
'traslado', 'uuid', 'version', 'xml'
|
||||
];
|
||||
|
||||
$words = preg_split('/\s+/', $phrase);
|
||||
$first = $words[0];
|
||||
$rest = array_slice($words, 1);
|
||||
|
||||
$isCapitalized = ctype_upper(Str::substr($first, 0, 1));
|
||||
$isUppercase = strtoupper($first) === $first;
|
||||
|
||||
$wordLower = Str::lower($first);
|
||||
|
||||
// Excepciones que no se pluralizan
|
||||
if (in_array($wordLower, $exceptions)) {
|
||||
$plural = $first;
|
||||
} else {
|
||||
$pluralBase = Str::pluralEs($wordLower);
|
||||
|
||||
$plural = match (true) {
|
||||
$isUppercase => strtoupper($pluralBase),
|
||||
$isCapitalized => Str::of($pluralBase)->ucfirst()->value(),
|
||||
default => $pluralBase,
|
||||
};
|
||||
}
|
||||
|
||||
return implode(' ', array_merge([$plural], $rest));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
29
src/Application/Macros/___LogMacrosServiceProvider.php
Normal file
29
src/Application/Macros/___LogMacrosServiceProvider.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Logger;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Koneko\VuexyAdmin\Application\Services\SystemLoggerService;
|
||||
|
||||
class LogMacrosServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
// Macro principal: log central
|
||||
Log::macro('system', function (): SystemLoggerService {
|
||||
return app(SystemLoggerService::class);
|
||||
});
|
||||
|
||||
// 🔧 Ejemplo de uso inmediato (puedes remover o modificar):
|
||||
/*
|
||||
Log::system()->info(
|
||||
module: 'vuexy-admin',
|
||||
message: 'Sistema de macros de log inicializado correctamente',
|
||||
context: ['environment' => app()->environment()]
|
||||
);
|
||||
*/
|
||||
}
|
||||
}
|
42
src/Application/Macros/___SettingsScopesMacro.php
Normal file
42
src/Application/Macros/___SettingsScopesMacro.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Macros;
|
||||
|
||||
use Illuminate\Support\Facades\App;
|
||||
|
||||
App::macro('settings', function () {
|
||||
$settingsService = app(\Koneko\VuexyAdmin\Application\System\SettingsService::class);
|
||||
|
||||
return new class($settingsService) {
|
||||
public function __construct(private $settingsService) {}
|
||||
|
||||
public function get(string $key, ...$args): mixed
|
||||
{
|
||||
return $this->settingsService->get($key, ...$args);
|
||||
}
|
||||
|
||||
public function set(string $key, mixed $value, ...$args): mixed
|
||||
{
|
||||
return $this->settingsService->set($key, $value, ...$args);
|
||||
}
|
||||
|
||||
public function admin(): object
|
||||
{
|
||||
return new class($this->settingsService) {
|
||||
public function __construct(private $settingsService) {}
|
||||
|
||||
public function get(string $key, ...$args): mixed
|
||||
{
|
||||
return $this->settingsService->get('koneko.admin.' . $key, ...$args);
|
||||
}
|
||||
|
||||
public function set(string $key, mixed $value, ...$args): mixed
|
||||
{
|
||||
return $this->settingsService->set('koneko.admin.' . $key, $value, ...$args);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
27
src/Application/Macros/___VuexyAdminLoggerMacro.php
Normal file
27
src/Application/Macros/___VuexyAdminLoggerMacro.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Macros;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
Log::macro('vuexyAdminLogger', function () {
|
||||
return new class {
|
||||
public function info(string $message, array $context = []) {
|
||||
return Log::system()->info('vuexy-admin', $message, $context);
|
||||
}
|
||||
|
||||
public function warning(string $message, array $context = []) {
|
||||
return Log::system()->warning('vuexy-admin', $message, $context);
|
||||
}
|
||||
|
||||
public function error(string $message, array $context = []) {
|
||||
return Log::system()->error('vuexy-admin', $message, $context);
|
||||
}
|
||||
|
||||
public function debug(string $message, array $context = []) {
|
||||
return Log::system()->debug('vuexy-admin', $message, $context);
|
||||
}
|
||||
};
|
||||
});
|
167
src/Application/Macros/___VuexyAdminSettingsMacro.php
Normal file
167
src/Application/Macros/___VuexyAdminSettingsMacro.php
Normal file
@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Macros;
|
||||
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Collection;
|
||||
use Koneko\VuexyAdmin\Application\Contracts\Settings\SettingsRepositoryInterface;
|
||||
|
||||
App::macro('vuexySettings', function () {
|
||||
$settingsService = app(SettingsRepositoryInterface::class);
|
||||
|
||||
return new class($settingsService) {
|
||||
/**
|
||||
* @var string Default namespace assigned during module boot
|
||||
*/
|
||||
private string $defaultNamespace = '';
|
||||
|
||||
/**
|
||||
* @var string|null Current namespace used for operations
|
||||
*/
|
||||
private ?string $currentNamespace = null;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param SettingsRepositoryInterface $settingsService
|
||||
*/
|
||||
public function __construct(private SettingsRepositoryInterface $settingsService) {}
|
||||
|
||||
/**
|
||||
* Sets the default namespace (typically from the module at boot time).
|
||||
*
|
||||
* @param string $namespace
|
||||
* @return self
|
||||
*/
|
||||
public function setDefaultNamespace(string $namespace): self
|
||||
{
|
||||
$this->defaultNamespace = rtrim($namespace, '.') . '.';
|
||||
$this->currentNamespace = $this->defaultNamespace;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the namespace to the module's own namespace.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function self(): self
|
||||
{
|
||||
$this->currentNamespace = $this->defaultNamespace;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches the namespace to a custom one.
|
||||
*
|
||||
* @param string $namespace
|
||||
* @return self
|
||||
*/
|
||||
public function in(string $namespace): self
|
||||
{
|
||||
$this->currentNamespace = rtrim($namespace, '.') . '.';
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a setting key with the current namespace applied.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed ...$args
|
||||
* @return mixed
|
||||
*/
|
||||
public function get(string $key, ...$args): mixed
|
||||
{
|
||||
return $this->settingsService->get($this->qualifyKey($key), ...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a setting key with the current namespace applied.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @param mixed ...$args
|
||||
* @return mixed
|
||||
*/
|
||||
public function set(string $key, mixed $value, ...$args): mixed
|
||||
{
|
||||
return $this->settingsService->set($this->qualifyKey($key), $value, ...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all groups (namespaces) available.
|
||||
*
|
||||
* @return SettingsGroupCollection
|
||||
*/
|
||||
public function listGroups(): SettingsGroupCollection
|
||||
{
|
||||
// Extraemos todos los keys agrupados
|
||||
$allSettings = $this->settingsService->getGroup('');
|
||||
|
||||
// Mapeamos
|
||||
$groups = collect($allSettings)
|
||||
->keys()
|
||||
->map(function ($key) {
|
||||
$parts = explode('.', $key);
|
||||
return $parts[0] ?? 'unknown';
|
||||
})
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
// Aplicamos filtro si es namespace específico
|
||||
if ($this->currentNamespace !== null) {
|
||||
$namespaceRoot = rtrim($this->currentNamespace, '.');
|
||||
$groups = $groups->filter(fn($group) => $group === $namespaceRoot);
|
||||
}
|
||||
|
||||
return new SettingsGroupCollection($groups);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current namespace.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function currentNamespace(): string
|
||||
{
|
||||
return $this->currentNamespace ?? $this->defaultNamespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to prepend namespace to key.
|
||||
*
|
||||
* @param string $key
|
||||
* @return string
|
||||
*/
|
||||
protected function qualifyKey(string $key): string
|
||||
{
|
||||
return $this->currentNamespace . $key;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Small helper class for group collection.
|
||||
*/
|
||||
class SettingsGroupCollection extends Collection
|
||||
{
|
||||
/**
|
||||
* Adds details (number of keys per group, etc.) to each group.
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function details(): Collection
|
||||
{
|
||||
return $this->map(function ($group) {
|
||||
// Aquí puedes agregar más metadata en el futuro
|
||||
return [
|
||||
'name' => $group,
|
||||
'key_prefix' => $group . '.',
|
||||
'total_keys' => settings()->in($group)->countKeys(),
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
149
src/Application/Queries/BootstrapTableQueryBuilder.php
Normal file
149
src/Application/Queries/BootstrapTableQueryBuilder.php
Normal file
@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Queries;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
/**
|
||||
* Clase base moderna para construir consultas de Bootstrap Table
|
||||
* compatible con el nuevo sistema ERP basado en TableConfigBuilder.
|
||||
*
|
||||
* Permite usar `getIndexBaseQuery()` directamente desde las clases
|
||||
* extendidas de `AbstractTableConfigBuilder`.
|
||||
*/
|
||||
class BootstrapTableQueryBuilder
|
||||
{
|
||||
/** @var Request */
|
||||
protected Request $request;
|
||||
|
||||
/** @var Builder */
|
||||
protected Builder $query;
|
||||
|
||||
/** @var array */
|
||||
protected array $config;
|
||||
|
||||
/**
|
||||
* Constructor principal.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Builder $baseQuery
|
||||
* @param array $config
|
||||
*/
|
||||
public function __construct(Request $request, Builder $baseQuery, array $config)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->query = $baseQuery;
|
||||
$this->config = $config;
|
||||
|
||||
$this->applyJoins();
|
||||
$this->applyFilters();
|
||||
}
|
||||
|
||||
/**
|
||||
* Aplica los joins definidos en la configuración.
|
||||
*/
|
||||
protected function applyJoins(): void
|
||||
{
|
||||
foreach ($this->config['joins'] ?? [] as $join) {
|
||||
if (!is_array($join) || count($join) < 4) {
|
||||
throw new \InvalidArgumentException('JOIN mal formado: ' . json_encode($join));
|
||||
}
|
||||
|
||||
[$table, $first, $operator, $second] = $join;
|
||||
$extras = $join[4] ?? [];
|
||||
|
||||
$type = $extras['type'] ?? (
|
||||
str_contains(strtolower($table), 'left') ? 'leftJoin' : 'join'
|
||||
);
|
||||
|
||||
$this->query->{$type}($table, function ($joinObj) use ($first, $operator, $second, $extras) {
|
||||
$joinObj->on($first, $operator, $second);
|
||||
|
||||
if (!empty($extras['and']) && is_array($extras['and'])) {
|
||||
foreach ($extras['and'] as $condition) {
|
||||
$joinObj->whereRaw($condition);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aplica filtros definidos por configuración y request.
|
||||
*/
|
||||
protected function applyFilters(): void
|
||||
{
|
||||
if (!empty($this->config['filters'])) {
|
||||
foreach ($this->config['filters'] as $filter => $columns) {
|
||||
|
||||
if ($filter === 'search' && $this->request->filled('search')) {
|
||||
$searchValue = $this->request->input('search');
|
||||
|
||||
$this->query->where(function ($query) use ($columns, $searchValue) {
|
||||
foreach ($columns as $column) {
|
||||
$query->orWhere($column, 'LIKE', "%{$searchValue}%");
|
||||
}
|
||||
});
|
||||
|
||||
} elseif ($this->request->filled($filter)) {
|
||||
$column = is_array($columns) ? $columns[0] : $columns;
|
||||
|
||||
$this->query->where($column, 'LIKE', "%{$this->request->input($filter)}%");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aplica agrupaciones si se especificaron.
|
||||
*/
|
||||
protected function applyGrouping(): void
|
||||
{
|
||||
if (!empty($this->config['group_by'])) {
|
||||
$this->query->groupBy($this->config['group_by']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ejecuta la consulta con paginación, ordenamiento y devuelve el JSON
|
||||
* compatible con Bootstrap Table.
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function getJson(): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$this->applyGrouping();
|
||||
|
||||
$baseQuery = clone $this->query;
|
||||
$baseQuery->selectRaw('1'); // ← evita select *
|
||||
|
||||
$total = DB::table(DB::raw("({$baseQuery->toSql()}) as sub"))
|
||||
->setBindings($baseQuery->getBindings())
|
||||
->count();
|
||||
|
||||
$total = $baseQuery->count();
|
||||
|
||||
// Paginar resultados reales
|
||||
$this->query
|
||||
->select($this->config['columns'])
|
||||
->when($this->request->input('sort'), function ($query) {
|
||||
$query->orderBy($this->request->input('sort'), $this->request->input('order', 'asc'));
|
||||
})
|
||||
->when($this->request->input('offset'), fn($q) => $q->offset($this->request->input('offset')))
|
||||
->limit($this->request->input('limit', 10));
|
||||
|
||||
$rows = $this->query->toBase()->get()->map(function ($item) {
|
||||
return (array) $item; // ← convierte stdClass en array sin perder columnas
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'total' => $total,
|
||||
'rows' => $rows,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
262
src/Application/RBAC/KonekoRbacSyncManager.php
Normal file
262
src/Application/RBAC/KonekoRbacSyncManager.php
Normal file
@ -0,0 +1,262 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\RBAC;
|
||||
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Koneko\VuexyAdmin\Application\Bootstrap\{KonekoModuleBootManager, KonekoModuleRegistry};
|
||||
use Koneko\VuexyAdmin\Models\{PermissionMeta, RoleMeta, PermissionGroup};
|
||||
use Spatie\Permission\Exceptions\PermissionDoesNotExist;
|
||||
|
||||
class KonekoRbacSyncManager
|
||||
{
|
||||
protected string $moduleName;
|
||||
protected string $basePath;
|
||||
|
||||
public function __construct(string $moduleName, string $basePath)
|
||||
{
|
||||
$this->moduleName = $moduleName;
|
||||
$this->basePath = $basePath;
|
||||
}
|
||||
|
||||
public static function importAll(): void
|
||||
{
|
||||
foreach (KonekoModuleRegistry::enabled() as $module) {
|
||||
$basePath = KonekoModuleBootManager::resolvePath($module, "database/data/rbac");
|
||||
File::ensureDirectoryExists($basePath);
|
||||
|
||||
$syncService = new self($module->composerName, $basePath);
|
||||
$syncService->import();
|
||||
}
|
||||
}
|
||||
|
||||
public function import(bool $onlyPermissions = false, bool $onlyRoles = false, bool $overwrite = false): void
|
||||
{
|
||||
$permissionPath = $this->basePath . '/permissions.json';
|
||||
$rolePath = $this->basePath . '/roles.json';
|
||||
|
||||
// Importar Permisos
|
||||
if (!$onlyRoles && File::exists($permissionPath)) {
|
||||
$json = json_decode(File::get($permissionPath), true);
|
||||
|
||||
$moduleKey = $json['module'] ?? 'undefined';
|
||||
$groups = $json['groups'] ?? [];
|
||||
$externar_groups = $json['externar_groups'] ?? [];
|
||||
|
||||
$groupModel = PermissionGroup::firstOrCreate(
|
||||
[
|
||||
'module_register' => $this->moduleName,
|
||||
'type' => 'module',
|
||||
'module' => $moduleKey,
|
||||
],
|
||||
[
|
||||
'name' => $json['name'] ?? ['es' => $moduleKey, 'en' => $moduleKey],
|
||||
'ui_metadata' => $json['_meta'] ?? null,
|
||||
'priority' => $json['priority'] ?? null,
|
||||
]
|
||||
);
|
||||
|
||||
// Importar grupos
|
||||
foreach ($groups as $groupKey => $group) {
|
||||
$rootModel = PermissionGroup::firstOrCreate(
|
||||
[
|
||||
'module_register' => $this->moduleName,
|
||||
'parent_id' => $groupModel->id,
|
||||
'type' => 'root_group',
|
||||
'module' => $moduleKey,
|
||||
'grupo' => $groupKey,
|
||||
],
|
||||
[
|
||||
'name' => $group['name'] ?? ['es' => $groupKey, 'en' => $groupKey],
|
||||
'ui_metadata' => $group['_meta'] ?? null,
|
||||
'priority' => $group['priority'] ?? null,
|
||||
]
|
||||
);
|
||||
|
||||
// Importar subgrupos
|
||||
foreach ($group['sub_groups'] ?? [] as $subGroupKey => $subGroup) {
|
||||
// Vincular subgrupos huérfanos (sin parent_id)
|
||||
$nullSubGroupDb = PermissionGroup::whereNull('parent_id')
|
||||
->where('type', 'sub_group')
|
||||
->where('module', $moduleKey)
|
||||
->where('grupo', $groupKey)
|
||||
->get();
|
||||
|
||||
foreach ($nullSubGroupDb as $subGroup) {
|
||||
$subGroup->parent_id = $rootModel->id;
|
||||
$subGroup->save();
|
||||
}
|
||||
|
||||
$subGroupModel = PermissionGroup::firstOrCreate(
|
||||
[
|
||||
'module_register' => $this->moduleName,
|
||||
'parent_id' => $rootModel->id,
|
||||
'type' => 'sub_group',
|
||||
'module' => $moduleKey,
|
||||
'grupo' => $groupKey,
|
||||
'sub_grupo' => $subGroupKey,
|
||||
],
|
||||
[
|
||||
'name' => $subGroup['name'] ?? ['es' => $subGroupKey, 'en' => $subGroupKey],
|
||||
'ui_metadata' => $subGroup['_meta'] ?? null,
|
||||
'priority' => $subGroup['priority'] ?? null,
|
||||
]
|
||||
);
|
||||
|
||||
// Crear permismos del subgrupo
|
||||
foreach ($subGroup['permissions'] ?? [] as $permission) {
|
||||
PermissionMeta::updateOrCreate(
|
||||
[
|
||||
'name' => $moduleKey . '.' . $permission['key'],
|
||||
'guard_name' => $permission['guard_name'] ?? 'web',
|
||||
],
|
||||
[
|
||||
'group_id' => $subGroupModel->id,
|
||||
'label' => $permission['label'] ?? null,
|
||||
'ui_metadata' => isset($permission['_meta']) ? $permission['_meta'] : null,
|
||||
'action' => $permission['action'] ?? null,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Importar permisos externos
|
||||
foreach ($externar_groups as $module) {
|
||||
$moduleKey = $module['module'];
|
||||
|
||||
// Procesar grupos externos
|
||||
foreach($module['grupos'] as $groupKey => $grupos) {
|
||||
|
||||
// Procesar subgrupos externos
|
||||
foreach ($grupos as $subGroupKey => $subgrupos) {
|
||||
|
||||
$parentId = PermissionGroup::where('type', 'sub_group')
|
||||
->where('module_register', '!=', $this->moduleName)
|
||||
->where('module', $moduleKey)
|
||||
->where('grupo', $groupKey)
|
||||
->first()?->id;
|
||||
|
||||
$subGroupModel = PermissionGroup::firstOrCreate(
|
||||
[
|
||||
'module_register' => $this->moduleName,
|
||||
'parent_id' => $parentId,
|
||||
'type' => 'sub_group',
|
||||
'module' => $moduleKey,
|
||||
'grupo' => $groupKey,
|
||||
'sub_grupo' => $subGroupKey,
|
||||
],
|
||||
[
|
||||
'name' => $subGroup['name'] ?? ['es' => $subGroupKey, 'en' => $subGroupKey],
|
||||
'ui_metadata' => $subGroup['_meta'] ?? null,
|
||||
'priority' => $subGroup['priority'] ?? null,
|
||||
]
|
||||
);
|
||||
|
||||
// Procesar permisos
|
||||
foreach ($subgrupos['permissions'] as $permission) {
|
||||
|
||||
PermissionMeta::updateOrCreate(
|
||||
[
|
||||
'name' => $moduleKey . '.' . $permission['key'],
|
||||
'guard_name' => $permission['guard_name'] ?? 'web',
|
||||
],
|
||||
[
|
||||
'group_id' => $subGroupModel->id,
|
||||
'label' => $permission['label'] ?? null,
|
||||
'ui_metadata' => isset($permission['_meta']) ? $permission['_meta'] : null,
|
||||
'action' => $permission['action'] ?? null,
|
||||
]
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
if (!$parentId){
|
||||
Log::info("[RBAC] Permiso externo agregado: {$moduleKey}.{$subGroupKey} con ausencia de componente {$moduleKey}.{$groupKey}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Importar Roles
|
||||
if (!$onlyPermissions && File::exists($rolePath)) {
|
||||
$roles = json_decode(File::get($rolePath), true);
|
||||
|
||||
foreach ($roles as $name => $config) {
|
||||
$role = RoleMeta::firstOrCreate(
|
||||
[
|
||||
'name' => $name,
|
||||
'guard_name' => $config['guard_name'] ?? 'web',
|
||||
],
|
||||
[
|
||||
'ui_metadata' => $config['_meta'] ?? null,
|
||||
]
|
||||
);
|
||||
|
||||
try {
|
||||
if ($overwrite) {
|
||||
$role->syncPermissions($config['permissions']);
|
||||
|
||||
} else {
|
||||
$role->givePermissionTo($config['permissions']);
|
||||
}
|
||||
|
||||
} catch (PermissionDoesNotExist $e) {
|
||||
Log::warning("[RBAC] Rol '{$name}' contiene permisos no válidos: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function export(bool $onlyPermissions = false, bool $onlyRoles = false): void
|
||||
{
|
||||
File::ensureDirectoryExists($this->basePath);
|
||||
|
||||
if (!$onlyRoles) {
|
||||
$permissions = PermissionMeta::query()
|
||||
->where('name', 'like', "$this->moduleName.%")
|
||||
->get()
|
||||
->mapWithKeys(fn($perm) => [
|
||||
$perm->name => [
|
||||
'guard_name' => $perm->guard_name,
|
||||
'action' => $perm->action,
|
||||
'label' => $perm->label,
|
||||
'ui_metadata' => $perm->ui_metadata,
|
||||
'group_id' => $perm->group_id,
|
||||
]
|
||||
])->toArray();
|
||||
|
||||
File::put("$this->basePath/permissions.json", json_encode($permissions, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
|
||||
if (!$onlyPermissions) {
|
||||
$roles = RoleMeta::all()->mapWithKeys(function ($role) {
|
||||
return [$role->name => [
|
||||
'style' => $role->ui_metadata['style'] ?? null,
|
||||
'permissions' => $role->permissions->pluck('name')->sort()->values()->all(),
|
||||
]];
|
||||
})->toArray();
|
||||
|
||||
File::put("$this->basePath/roles.json", json_encode($roles, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
|
||||
public function publish(bool $force = false): void
|
||||
{
|
||||
File::ensureDirectoryExists($this->basePath);
|
||||
|
||||
$defaultPermissions = [];
|
||||
$defaultRoles = [];
|
||||
|
||||
if (!File::exists("$this->basePath/permissions.json") || $force) {
|
||||
File::put("$this->basePath/permissions.json", json_encode($defaultPermissions, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
|
||||
if (!File::exists("$this->basePath/roles.json") || $force) {
|
||||
File::put("$this->basePath/roles.json", json_encode($defaultRoles, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
}
|
64
src/Application/Security/VaultKeyService.php
Normal file
64
src/Application/Security/VaultKeyService.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Security;
|
||||
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Koneko\VuexyAdmin\Models\VaultKey;
|
||||
|
||||
class VaultKeyService
|
||||
{
|
||||
public function generateKey(
|
||||
string $alias,
|
||||
string $ownerProject = 'default_project',
|
||||
string $algorithm = 'AES-256-CBC',
|
||||
bool $isSensitive = true
|
||||
): VaultKey {
|
||||
if (!in_array($algorithm, openssl_get_cipher_methods())) {
|
||||
throw new \InvalidArgumentException("Algoritmo no soportado: $algorithm");
|
||||
}
|
||||
|
||||
$keyMaterial = random_bytes(openssl_cipher_iv_length($algorithm) * 2);
|
||||
$encryptedKey = Crypt::encrypt($keyMaterial);
|
||||
|
||||
return VaultKey::create([
|
||||
'environment' => app()->environment(),
|
||||
'namespace' => config('app.name'),
|
||||
'scope' => 'global',
|
||||
'alias' => $alias,
|
||||
'owner_project' => $ownerProject,
|
||||
'algorithm' => $algorithm,
|
||||
'key_material' => $encryptedKey,
|
||||
'is_active' => true,
|
||||
'is_sensitive' => $isSensitive,
|
||||
'rotated_at' => now(),
|
||||
'rotation_count' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
public function retrieveKey(string $alias): string
|
||||
{
|
||||
$keyEntry = VaultKey::where('alias', $alias)->where('is_active', true)->firstOrFail();
|
||||
|
||||
return Crypt::decrypt($keyEntry->key_material);
|
||||
}
|
||||
|
||||
public function rotateKey(string $alias): VaultKey
|
||||
{
|
||||
$keyEntry = VaultKey::where('alias', $alias)->firstOrFail();
|
||||
|
||||
$newKeyMaterial = random_bytes(openssl_cipher_iv_length($keyEntry->algorithm) * 2);
|
||||
|
||||
$keyEntry->update([
|
||||
'key_material' => Crypt::encrypt($newKeyMaterial),
|
||||
'rotated_at' => now(),
|
||||
'rotation_count' => $keyEntry->rotation_count + 1,
|
||||
]);
|
||||
|
||||
return $keyEntry;
|
||||
}
|
||||
|
||||
public function deactivateKey(string $alias): bool
|
||||
{
|
||||
return VaultKey::where('alias', $alias)->update(['is_active' => false]);
|
||||
}
|
||||
}
|
289
src/Application/Seeding/SeederOrchestrator.php
Normal file
289
src/Application/Seeding/SeederOrchestrator.php
Normal file
@ -0,0 +1,289 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Koneko\VuexyAdmin\Application\Seeding;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Koneko\VuexyAdmin\Application\Seeding\SeederReportBuilder;
|
||||
use Koneko\VuexyAdmin\Application\Traits\Seeders\Main\HasSeederLogger;
|
||||
use Koneko\VuexyAdmin\Application\UI\Avatar\AvatarImageService;
|
||||
use Koneko\VuexyAdmin\Application\UI\Avatar\AvatarInitialsService;
|
||||
use Koneko\VuexyAdmin\Support\Seeders\AbstractDataSeeder;
|
||||
|
||||
class SeederOrchestrator
|
||||
{
|
||||
use HasSeederLogger;
|
||||
|
||||
protected ?Command $command = null;
|
||||
protected SeederReportBuilder $reporter;
|
||||
|
||||
public function __construct(?Command $command = null)
|
||||
{
|
||||
$this->command = $command;
|
||||
$this->reporter = new SeederReportBuilder();
|
||||
}
|
||||
|
||||
public function run(array|string|null $only = null, array $options = []): void
|
||||
{
|
||||
// Configuración del entorno
|
||||
if (isset($options['env'])) {
|
||||
config(['seeder.env' => $options['env']]);
|
||||
}
|
||||
|
||||
// Determinar si se debe generar reporte
|
||||
$shouldGenerateReport = $options['report'] ?? config('seeder.defaults.report', false);
|
||||
|
||||
// Ejecutar limpieza si está activada
|
||||
$this->clearAssetsIfNeeded($options);
|
||||
|
||||
// Filtrar módulos y resolver dependencias
|
||||
$modules = $this->filterModules(config('seeder.modules', []), $only);
|
||||
$modules = $this->resolveDependencies($modules);
|
||||
|
||||
foreach ($modules as $key => $config) {
|
||||
$this->log("Iniciando módulo: <info>{$key}</info>");
|
||||
|
||||
$mergedConfig = array_merge(config('seeder.defaults', []), $config, $options);
|
||||
$this->processModule($key, $mergedConfig);
|
||||
}
|
||||
|
||||
// Guardar reporte solo si está habilitado
|
||||
if ($shouldGenerateReport) {
|
||||
$this->reporter->saveReports();
|
||||
}
|
||||
}
|
||||
|
||||
protected function processModule(string $key, array $config): void
|
||||
{
|
||||
try {
|
||||
$seederClass = $config['seeder'];
|
||||
$seeder = app($seederClass);
|
||||
|
||||
// Inyectar comando si es posible
|
||||
if (method_exists($seeder, 'setCommand')) {
|
||||
$seeder->setCommand($this->command);
|
||||
|
||||
} elseif (property_exists($seeder, 'command')) {
|
||||
$seeder->command = $this->command;
|
||||
}
|
||||
|
||||
// Configuración general para seeders avanzados
|
||||
if ($seeder instanceof AbstractDataSeeder) {
|
||||
if ($this->command) {
|
||||
$seeder->setCommand($this->command);
|
||||
}
|
||||
|
||||
if (isset($config['file'])) {
|
||||
$seeder->setTargetFile($config['file']);
|
||||
}
|
||||
|
||||
if ($config['truncate'] ?? false) {
|
||||
$this->log("🗑️ Truncando tabla para {$key}...");
|
||||
$seeder->truncate();
|
||||
}
|
||||
}
|
||||
|
||||
// Modo simulación
|
||||
if ($config['dry-run'] ?? false) {
|
||||
$this->logDryRun($key, $config);
|
||||
|
||||
$this->reporter->addModule([
|
||||
'name' => $key,
|
||||
'status' => 'skipped',
|
||||
'file' => $config['file'] ?? null,
|
||||
'records' => null,
|
||||
'time' => null,
|
||||
'truncated' => $config['truncate'] ?? false,
|
||||
'faker' => false,
|
||||
'details' => 'Dry run',
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ejecución real
|
||||
$this->executeSeeder($seeder, $key, $config);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$this->log("❌ Error en módulo {$key}: {$e->getMessage()}");
|
||||
|
||||
$this->reporter->addModule([
|
||||
'name' => $key,
|
||||
'status' => 'failed',
|
||||
'file' => $config['file'] ?? null,
|
||||
'records' => null,
|
||||
'time' => null,
|
||||
'truncated' => $config['truncate'] ?? false,
|
||||
'faker' => false,
|
||||
'details' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
protected function executeSeeder($seeder, string $key, array $config): void
|
||||
{
|
||||
$records = null;
|
||||
|
||||
if ($config['transactional'] ?? false) {
|
||||
$connection = $seeder->getModel()->getConnection();
|
||||
$connection->transaction(function () use ($seeder, $key, $config, &$records) {
|
||||
$records = $this->runSeederCore($seeder, $key, $config);
|
||||
});
|
||||
} else {
|
||||
$records = $this->runSeederCore($seeder, $key, $config);
|
||||
}
|
||||
|
||||
$this->reporter->addModule([
|
||||
'name' => $key,
|
||||
'status' => 'completed',
|
||||
'records' => $records,
|
||||
'time' => round(microtime(true) - LARAVEL_START, 2) . 's',
|
||||
'truncated' => $config['truncate'] ?? false,
|
||||
'faker' => isset($config['fake']),
|
||||
'file' => $config['file'] ?? 'N/A',
|
||||
'note' => 'OK',
|
||||
]);
|
||||
|
||||
$this->log("✅ Módulo completado: <info>{$key}</info>\n");
|
||||
}
|
||||
|
||||
protected function runSeederCore($seeder, string $key, array $config): int
|
||||
{
|
||||
$records = 0;
|
||||
|
||||
$runFake = $config['fake'] ?? false;
|
||||
$runNormal = !$runFake || !($config['faker_only'] ?? false); // si no está en modo faker_only, corre run()
|
||||
|
||||
// Ejecutar inserción desde archivo solo si se permite
|
||||
if ($runNormal && method_exists($seeder, 'run')) {
|
||||
$seeder->run($config);
|
||||
$records = method_exists($seeder, 'getProcessedCount') ? $seeder->getProcessedCount() : 0;
|
||||
}
|
||||
|
||||
// Ejecutar faker si se permite
|
||||
if ($runFake && method_exists($seeder, 'runFake')) {
|
||||
$min = (int) ($config['fake-count'] ?? $config['fake']['min'] ?? 1);
|
||||
$max = (int) ($config['fake-count'] ?? $config['fake']['max'] ?? $min);
|
||||
$total = $min === $max ? $min : rand($min, $max);
|
||||
|
||||
$seeder->runFake($total, $config['fake']);
|
||||
$records += method_exists($seeder, 'getProcessedCount') ? $seeder->getProcessedCount() : $total;
|
||||
}
|
||||
|
||||
return $records;
|
||||
}
|
||||
|
||||
protected function resolveDependencies(array $modules): array
|
||||
{
|
||||
$resolved = [];
|
||||
$unresolved = $modules;
|
||||
|
||||
while (!empty($unresolved)) {
|
||||
$resolvedCount = count($resolved);
|
||||
|
||||
foreach ($unresolved as $key => $module) {
|
||||
$dependencies = $module['depends_on'] ?? [];
|
||||
|
||||
if (collect($dependencies)->every(fn($dep) => isset($resolved[$dep]))) {
|
||||
$resolved[$key] = $module;
|
||||
unset($unresolved[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($resolvedCount === count($resolved)) {
|
||||
$keys = implode(', ', array_keys($unresolved));
|
||||
throw new \RuntimeException("Dependencias circulares o no resueltas detectadas en: {$keys}");
|
||||
}
|
||||
}
|
||||
|
||||
return $resolved;
|
||||
}
|
||||
|
||||
protected function filterModules(array $modules, $only): array
|
||||
{
|
||||
$filtered = [];
|
||||
|
||||
foreach ($modules as $key => $module) {
|
||||
// 1. Si está deshabilitado, lo ignoramos completamente
|
||||
if (!($module['enabled'] ?? true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Si hay filtro por "only", respetarlo
|
||||
if ($only !== null) {
|
||||
$onlyList = is_array($only) ? $only : [$only];
|
||||
if (!in_array($key, $onlyList)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$filtered[$key] = $module;
|
||||
}
|
||||
|
||||
return $filtered;
|
||||
}
|
||||
|
||||
protected function clearAssetsIfNeeded(array $options = []): void
|
||||
{
|
||||
if ($options['dry-run'] ?? false) {
|
||||
$this->log("🔹 [DRY RUN] Simularía limpieza de assets");
|
||||
return;
|
||||
}
|
||||
|
||||
$config = config('seeder.clear_assets', []);
|
||||
|
||||
foreach ($config as $assetType => $enabled) {
|
||||
if (!$enabled) continue;
|
||||
|
||||
match ($assetType) {
|
||||
'avatars' => app(AvatarImageService::class)->clearAllProfilePhotos(),
|
||||
'initials' => app(AvatarInitialsService::class)->clearAllInitials(),
|
||||
'attachments' => Storage::disk('attachments')->deleteDirectory('all'),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected function logDryRun(string $key, array $config): void
|
||||
{
|
||||
$this->log("🔹 [DRY RUN] Simulación para módulo: <info>{$key}</info>");
|
||||
$this->log("├─ Seeder: <comment>{$config['seeder']}</comment>");
|
||||
if (isset($config['file'])) {
|
||||
$this->log("├─ Archivo: <comment>{$config['file']}</comment>");
|
||||
}
|
||||
if (isset($config['fake'])) {
|
||||
$count = $config['fake-count'] ?? ($config['fake']['max'] ?? 'aleatorio');
|
||||
$this->log("├─ Datos falsos: <comment>{$count} registros</comment>");
|
||||
}
|
||||
if ($config['truncate'] ?? false) {
|
||||
$this->log("└─ <fg=red>TRUNCATE activado</>");
|
||||
}
|
||||
}
|
||||
|
||||
public function getAvailableModules(): array
|
||||
{
|
||||
return config('seeder.modules', []);
|
||||
}
|
||||
|
||||
public function getSeederClass(string $module): ?string
|
||||
{
|
||||
return config("seeder.modules.$module.seeder");
|
||||
}
|
||||
|
||||
public function getReportPath(): string
|
||||
{
|
||||
return $this->reporter->getReportPathMarkdown();
|
||||
}
|
||||
|
||||
public function getReport(): array
|
||||
{
|
||||
return $this->reporter->getReportData();
|
||||
}
|
||||
|
||||
public function setCommand(Command $command): static
|
||||
{
|
||||
$this->command = $command;
|
||||
return $this;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user