Rutas del sistema y archivos de configuración

This commit is contained in:
Lucho
2026-06-24 16:29:04 -03:00
parent c81120f2e3
commit 9474236226
6 changed files with 6856 additions and 13 deletions
+4 -4
View File
@@ -65,7 +65,7 @@ return [
|
*/
'timezone' => 'UTC',
'timezone' => 'America/Argentina/Buenos_Aires',
/*
|--------------------------------------------------------------------------
@@ -78,11 +78,11 @@ return [
|
*/
'locale' => env('APP_LOCALE', 'en'),
'locale' => env('APP_LOCALE', 'es'),
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'es'),
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
'faker_locale' => env('APP_FAKER_LOCALE', 'es_AR'),
/*
|--------------------------------------------------------------------------
+344
View File
@@ -0,0 +1,344 @@
<?php
return [
'backup' => [
/*
* The name of this application. You can use this name to monitor
* the backups.
*/
'name' => env('APP_NAME', 'AbogadasDelLitoral'),
'source' => [
'files' => [
/*
* The list of directories and files that will be included in the backup.
*/
'include' => array_values(array_filter([
storage_path('app/public/fotos'),
storage_path('app/private/documentacion-clientes'),
public_path('images/profesionales'),
public_path('images/servicios'),
public_path('images/bugs'),
public_path('uploads/documentacion-clientes'),
filter_var(env('BACKUP_INCLUDE_ENV', false), FILTER_VALIDATE_BOOLEAN) ? base_path('.env') : null,
], static fn (mixed $path): bool => is_string($path) && (is_dir($path) || is_file($path)))),
/*
* These directories and files will be excluded from the backup.
*
* Directories used by the backup process will automatically be excluded.
*/
'exclude' => [],
/*
* Determines if symlinks should be followed.
*/
'follow_links' => false,
/*
* Determines if it should avoid unreadable folders.
*/
'ignore_unreadable_directories' => false,
/*
* This path is used to make directories in resulting zip-file relative
* Set to `null` to include complete absolute path
* Example: base_path()
*/
'relative_path' => base_path(),
],
/*
* The names of the connections to the databases that should be backed up
* MySQL, PostgreSQL, SQLite and Mongo databases are supported.
*
* The content of the database dump may be customized for each connection
* by adding a 'dump' key to the connection settings in config/database.php.
* E.g.
* 'mysql' => [
* ...
* 'dump' => [
* 'exclude_tables' => [
* 'table_to_exclude_from_backup',
* 'another_table_to_exclude'
* ]
* ],
* ],
*
* If you are using only InnoDB tables on a MySQL server, you can
* also supply the useSingleTransaction option to avoid table locking.
*
* E.g.
* 'mysql' => [
* ...
* 'dump' => [
* 'useSingleTransaction' => true,
* ],
* ],
*
* For a complete list of available customization options, see https://github.com/spatie/db-dumper
*/
'databases' => [
env('DB_CONNECTION', 'mysql'),
],
],
/*
* The database dump can be compressed to decrease disk space usage.
*
* Out of the box Laravel-backup supplies
* Spatie\DbDumper\Compressors\GzipCompressor::class.
*
* You can also create custom compressor. More info on that here:
* https://github.com/spatie/db-dumper#using-compression
*
* If you do not want any compressor at all, set it to null.
*/
'database_dump_compressor' => null,
/*
* If specified, the database dumped file name will contain a timestamp (e.g.: 'Y-m-d-H-i-s').
*/
'database_dump_file_timestamp_format' => null,
/*
* The base of the dump filename, either 'database' or 'connection'
*
* If 'database' (default), the dumped filename will contain the database name.
* If 'connection', the dumped filename will contain the connection name.
*/
'database_dump_filename_base' => 'database',
/*
* The file extension used for the database dump files.
*
* If not specified, the file extension will be .archive for MongoDB and .sql for all other databases
* The file extension should be specified without a leading .
*/
'database_dump_file_extension' => '',
'destination' => [
/*
* The compression algorithm to be used for creating the zip archive.
*
* If backing up only database, you may choose gzip compression for db dump and no compression at zip.
*
* Some common algorithms are listed below:
* ZipArchive::CM_STORE (no compression at all; set 0 as compression level)
* ZipArchive::CM_DEFAULT
* ZipArchive::CM_DEFLATE
* ZipArchive::CM_BZIP2
* ZipArchive::CM_XZ
*
* For more check https://www.php.net/manual/zip.constants.php and confirm it's supported by your system.
*/
'compression_method' => ZipArchive::CM_DEFAULT,
/*
* The compression level corresponding to the used algorithm; an integer between 0 and 9.
*
* Check supported levels for the chosen algorithm, usually 1 means the fastest and weakest compression,
* while 9 the slowest and strongest one.
*
* Setting of 0 for some algorithms may switch to the strongest compression.
*/
'compression_level' => 9,
/*
* The filename prefix used for the backup zip file.
*/
'filename_prefix' => '',
/*
* The disk names on which the backups will be stored.
*/
'disks' => [
...array_values(array_filter(array_map('trim', explode(',', env('BACKUP_DESTINATION_DISK', 'local'))))),
],
],
/*
* The directory where the temporary files will be stored.
*/
'temporary_directory' => storage_path('app/backup-temp'),
/*
* The password to be used for archive encryption.
* Set to `null` to disable encryption.
*/
'password' => env('BACKUP_ARCHIVE_PASSWORD') ?: null,
/*
* The encryption algorithm to be used for archive encryption.
* You can set it to `null` or `false` to disable encryption.
*
* When set to 'default', we'll use ZipArchive::EM_AES_256 if it is
* available on your system.
*/
'encryption' => env('BACKUP_ARCHIVE_PASSWORD') ? 'default' : false,
/*
* The number of attempts, in case the backup command encounters an exception
*/
'tries' => 1,
/*
* The number of seconds to wait before attempting a new backup if the previous try failed
* Set to `0` for none
*/
'retry_delay' => 0,
],
/*
* You can get notified when specific events occur. Out of the box you can use 'mail' and 'slack'.
* For Slack you need to install laravel/slack-notification-channel.
*
* You can also use your own notification classes, just make sure the class is named after one of
* the `Spatie\Backup\Notifications\Notifications` classes.
*/
'notifications' => [
'notifications' => [
\Spatie\Backup\Notifications\Notifications\BackupHasFailedNotification::class => ['mail'],
\Spatie\Backup\Notifications\Notifications\UnhealthyBackupWasFoundNotification::class => ['mail'],
\Spatie\Backup\Notifications\Notifications\CleanupHasFailedNotification::class => ['mail'],
\Spatie\Backup\Notifications\Notifications\BackupWasSuccessfulNotification::class => [],
\Spatie\Backup\Notifications\Notifications\HealthyBackupWasFoundNotification::class => [],
\Spatie\Backup\Notifications\Notifications\CleanupWasSuccessfulNotification::class => [],
],
/*
* Here you can specify the notifiable to which the notifications should be sent. The default
* notifiable will use the variables specified in this config file.
*/
'notifiable' => \Spatie\Backup\Notifications\Notifiable::class,
'mail' => [
'to' => env('BACKUP_MAIL_TO', env('MAIL_FROM_ADDRESS', 'admin@example.com')),
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
'name' => env('MAIL_FROM_NAME', 'Example'),
],
],
'slack' => [
'webhook_url' => '',
/*
* If this is set to null the default channel of the webhook will be used.
*/
'channel' => null,
'username' => null,
'icon' => null,
],
'discord' => [
'webhook_url' => '',
/*
* If this is an empty string, the name field on the webhook will be used.
*/
'username' => '',
/*
* If this is an empty string, the avatar on the webhook will be used.
*/
'avatar_url' => '',
],
],
/*
* Here you can specify which backups should be monitored.
* If a backup does not meet the specified requirements the
* UnHealthyBackupWasFound event will be fired.
*/
'monitor_backups' => [
[
'name' => env('APP_NAME', 'laravel-backup'),
'disks' => array_values(array_filter(array_map('trim', explode(',', env('BACKUP_DESTINATION_DISK', 'local'))))),
'health_checks' => [
\Spatie\Backup\Tasks\Monitor\HealthChecks\MaximumAgeInDays::class => (int) env('BACKUP_MAX_AGE_DAYS', 7),
\Spatie\Backup\Tasks\Monitor\HealthChecks\MaximumStorageInMegabytes::class => (int) env('BACKUP_MAX_STORAGE_MB', 5000),
],
],
/*
[
'name' => 'name of the second app',
'disks' => ['local', 's3'],
'health_checks' => [
\Spatie\Backup\Tasks\Monitor\HealthChecks\MaximumAgeInDays::class => 1,
\Spatie\Backup\Tasks\Monitor\HealthChecks\MaximumStorageInMegabytes::class => 5000,
],
],
*/
],
'cleanup' => [
/*
* The strategy that will be used to cleanup old backups. The default strategy
* will keep all backups for a certain amount of days. After that period only
* a daily backup will be kept. After that period only weekly backups will
* be kept and so on.
*
* No matter how you configure it the default strategy will never
* delete the newest backup.
*/
'strategy' => \Spatie\Backup\Tasks\Cleanup\Strategies\DefaultStrategy::class,
'default_strategy' => [
/*
* The number of days for which backups must be kept.
*/
'keep_all_backups_for_days' => (int) env('BACKUP_KEEP_ALL_DAYS', 7),
/*
* After the "keep_all_backups_for_days" period is over, the most recent backup
* of that day will be kept. Older backups within the same day will be removed.
* If you create backups only once a day, no backups will be removed yet.
*/
'keep_daily_backups_for_days' => (int) env('BACKUP_KEEP_DAILY_DAYS', 16),
/*
* After the "keep_daily_backups_for_days" period is over, the most recent backup
* of that week will be kept. Older backups within the same week will be removed.
* If you create backups only once a week, no backups will be removed yet.
*/
'keep_weekly_backups_for_weeks' => (int) env('BACKUP_KEEP_WEEKLY_WEEKS', 8),
/*
* After the "keep_weekly_backups_for_weeks" period is over, the most recent backup
* of that month will be kept. Older backups within the same month will be removed.
*/
'keep_monthly_backups_for_months' => (int) env('BACKUP_KEEP_MONTHLY_MONTHS', 4),
/*
* After the "keep_monthly_backups_for_months" period is over, the most recent backup
* of that year will be kept. Older backups within the same year will be removed.
*/
'keep_yearly_backups_for_years' => (int) env('BACKUP_KEEP_YEARLY_YEARS', 2),
/*
* After cleaning up the backups remove the oldest backup until
* this amount of megabytes has been reached.
* Set null for unlimited size.
*/
'delete_oldest_backups_when_using_more_megabytes_than' => (int) env('BACKUP_MAX_STORAGE_LIMIT_MB', 5000),
],
/*
* The number of attempts, in case the cleanup command encounters an exception
*/
'tries' => 1,
/*
* The number of seconds to wait before attempting a new cleanup if the previous try failed
* Set to `0` for none
*/
'retry_delay' => 0,
],
];
+8 -1
View File
@@ -16,7 +16,7 @@ return [
|
*/
'default' => env('DB_CONNECTION', 'sqlite'),
'default' => env('DB_CONNECTION', 'mysql'),
/*
|--------------------------------------------------------------------------
@@ -60,7 +60,13 @@ return [
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci',
]) : [],
'dump' => [
'dump_binary_path' => env('DB_DUMP_BINARY_PATH', PHP_OS_FAMILY === 'Windows' ? 'C:/xampp/mysql/bin/' : '/usr/bin/'),
'use_single_transaction' => true,
'timeout' => 60 * 5, // 5 minutos de tiempo límite
],
],
'mariadb' => [
@@ -80,6 +86,7 @@ return [
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci',
]) : [],
],
+3 -3
View File
@@ -39,9 +39,9 @@ use App\Http\Controllers\UbicacionController;
use App\Http\Controllers\UserController;
Route::middleware('api')->group(function () {
Route::post('auth/login/cliente', [AuthController::class, 'loginCliente']);
Route::post('auth/login/personal', [AuthController::class, 'loginPersonal']);
Route::post('auth/login', [AuthController::class, 'login']);
Route::post('auth/login/cliente', [AuthController::class, 'loginCliente'])->middleware('throttle:login-cliente-api');
Route::post('auth/login/personal', [AuthController::class, 'loginPersonal'])->middleware('throttle:login-personal-api');
Route::post('auth/login', [AuthController::class, 'login'])->middleware('throttle:login-api-general');
Route::post('auth/logout', [AuthController::class, 'logout']);
// Rutas API Resource estándar
+225
View File
@@ -1,8 +1,233 @@
<?php
use App\Models\Administrador;
use App\Models\Formulario;
use App\Models\LogSeguridad;
use Database\Seeders\BulkDataSeeder;
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Schedule;
use Illuminate\Support\Facades\Storage;
Artisan::command('inspire', function () {
$this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote');
Artisan::command('logs:purge-old-security', function () {
$fechaLimite = now()->subMonths(2);
$eliminados = LogSeguridad::query()
->where('fechahora', '<', $fechaLimite)
->delete();
$this->info('Logs eliminados: ' . $eliminados);
})->purpose('Elimina logs de seguridad con más de 2 meses de antigüedad');
Artisan::command('seed:bulk {--modo=masivo} {--clientes=} {--profesionales=} {--servicios=} {--profesiones=} {--fresh}', function () {
$presets = [
'rapido' => ['clientes' => 100, 'profesionales' => 20, 'servicios' => 10, 'profesiones' => 2],
'medio' => ['clientes' => 500, 'profesionales' => 50, 'servicios' => 20, 'profesiones' => 3],
'masivo' => ['clientes' => 1000, 'profesionales' => 100, 'servicios' => 30, 'profesiones' => 4],
];
$modo = strtolower((string) $this->option('modo'));
if (!array_key_exists($modo, $presets)) {
$this->error('Modo invalido. Usa: rapido, medio o masivo.');
return 1;
}
$clientes = max(0, (int) ($this->option('clientes') ?? $presets[$modo]['clientes']));
$profesionales = max(0, (int) ($this->option('profesionales') ?? $presets[$modo]['profesionales']));
$servicios = max(0, (int) ($this->option('servicios') ?? $presets[$modo]['servicios']));
$profesiones = max(0, (int) ($this->option('profesiones') ?? $presets[$modo]['profesiones']));
config([
'bulk_seed.enabled' => false,
'bulk_seed.clientes' => $clientes,
'bulk_seed.profesionales' => $profesionales,
'bulk_seed.servicios' => $servicios,
'bulk_seed.profesiones' => $profesiones,
]);
if ((bool) $this->option('fresh')) {
$this->info('Ejecutando migrate:fresh --seed...');
$exitMigrate = Artisan::call('migrate:fresh', ['--seed' => true]);
$this->line(Artisan::output());
if ($exitMigrate !== 0) {
$this->error('Fallo migrate:fresh --seed');
return $exitMigrate;
}
}
$this->info("Cargando datos masivos (modo={$modo})...");
$exitBulk = Artisan::call('db:seed', ['--class' => BulkDataSeeder::class]);
$this->line(Artisan::output());
if ($exitBulk !== 0) {
$this->error('Fallo la ejecucion de BulkDataSeeder.');
return $exitBulk;
}
$this->info("Datos creados: {$profesiones} profesiones, {$servicios} servicios, {$clientes} clientes y {$profesionales} profesionales.");
return 0;
})->purpose('Carga datos de prueba masivos por modo o por cantidades personalizadas');
Artisan::command('formularios:sync-rejected-status', function () {
$formulariosPendientes = Formulario::query()
->where('estado', 'Pendiente')
->get(['id', 'profesion_id']);
$actualizados = 0;
foreach ($formulariosPendientes as $formulario) {
$formularioId = (int) $formulario->id;
$profesionFormularioId = (int) $formulario->profesion_id;
$asignadosActivos = DB::table('profesionales_formularios as pf')
->join('profesionales as p', 'p.id', '=', 'pf.profesional_id')
->where('pf.formulario_id', $formularioId)
->where('p.baja_id', 1)
->where(function ($query) use ($profesionFormularioId): void {
$query->where('p.profesion_id', $profesionFormularioId)
->orWhereExists(function ($subquery) use ($profesionFormularioId): void {
$subquery->select(DB::raw(1))
->from('profesionales_profesiones as pp')
->whereColumn('pp.profesional_id', 'p.id')
->where('pp.profesion_id', $profesionFormularioId);
});
})
->count();
if ($asignadosActivos === 0) {
continue;
}
$hayActivosSinRechazar = DB::table('profesionales_formularios as pf')
->join('profesionales as p', 'p.id', '=', 'pf.profesional_id')
->where('pf.formulario_id', $formularioId)
->where('p.baja_id', 1)
->where(function ($query) use ($profesionFormularioId): void {
$query->where('p.profesion_id', $profesionFormularioId)
->orWhereExists(function ($subquery) use ($profesionFormularioId): void {
$subquery->select(DB::raw(1))
->from('profesionales_profesiones as pp')
->whereColumn('pp.profesional_id', 'p.id')
->where('pp.profesion_id', $profesionFormularioId);
});
})
->whereRaw("LOWER(TRIM(pf.estado)) <> 'rechazado'")
->exists();
if (!$hayActivosSinRechazar) {
Formulario::query()
->where('id', $formularioId)
->update(['estado' => 'Rechazado por todos']);
$actualizados++;
}
}
$this->info('Formularios actualizados: ' . $actualizados);
})->purpose('Sincroniza formularios pendientes a Rechazado por todos cuando todos los profesionales activos rechazaron');
Artisan::command('backup:daily-email {--keep=4}', function () {
$includeEnv = (bool) filter_var((string) env('BACKUP_INCLUDE_ENV', false), FILTER_VALIDATE_BOOLEAN);
$archivePassword = trim((string) env('BACKUP_ARCHIVE_PASSWORD', ''));
if ($includeEnv && $archivePassword === '') {
$this->error('BACKUP_INCLUDE_ENV=true requiere BACKUP_ARCHIVE_PASSWORD configurado para cifrar el ZIP.');
return 1;
}
$this->info('Iniciando backup semanal de base de datos y archivos...');
$backupExitCode = Artisan::call('backup:run');
$this->line(Artisan::output());
if ($backupExitCode !== 0) {
$this->error('No se pudo generar el backup.');
return 1;
}
$disk = Storage::disk('local');
$backupFolder = trim((string) config('backup.backup.name', 'Laravel'), '/');
$files = collect($disk->files($backupFolder))
->filter(fn (string $file): bool => str_ends_with(strtolower($file), '.zip'))
->sortByDesc(fn (string $file): int => $disk->lastModified($file))
->values();
if ($files->isEmpty()) {
$this->error('No se encontró el archivo ZIP del backup para enviar por email.');
return 1;
}
$latestBackup = (string) $files->first();
$adminRecipients = Administrador::query()
->select('correo')
->whereNotNull('correo')
->pluck('correo')
->map(fn ($correo): string => trim((string) $correo))
->filter(fn (string $correo): bool => $correo !== '' && filter_var($correo, FILTER_VALIDATE_EMAIL) !== false)
->unique()
->values()
->all();
$fallbackRecipient = (string) env('BACKUP_MAIL_TO', (string) config('backup.notifications.mail.to'));
$recipients = !empty($adminRecipients)
? $adminRecipients
: array_values(array_filter([(string) $fallbackRecipient]));
if (empty($recipients)) {
$this->error('No hay correos de administradores ni BACKUP_MAIL_TO configurado para enviar el backup.');
return 1;
}
try {
Mail::raw('Se adjunta el backup semanal de base de datos y archivos.', function ($message) use ($disk, $latestBackup, $recipients): void {
$message->to($recipients)
->subject('Backup semanal - ' . now()->format('d/m/Y'))
->attachData(
$disk->get($latestBackup),
basename($latestBackup),
['mime' => 'application/zip']
);
});
} catch (\Throwable $e) {
$this->error('El backup se generó, pero falló el envío de email: ' . $e->getMessage());
return 1;
}
$keep = max(1, (int) $this->option('keep'));
$files->slice($keep)->each(function (string $file) use ($disk): void {
$disk->delete($file);
});
$deletedCount = max(0, $files->count() - $keep);
$this->info('Backup enviado a: ' . implode(', ', $recipients));
$this->info('Backups eliminados por retención: ' . $deletedCount);
$this->info('Backups conservados: ' . $keep);
return 0;
})->purpose('Genera backup semanal de BD y archivos, lo envía por mail y conserva solo los últimos N backups');
Schedule::command('logs:purge-old-security')->daily();
Schedule::command('backup:daily-email --keep=4')->dailyAt('16:00'); //ejecuta backup:run
Schedule::command('backup:clean')->dailyAt('16:20');
Schedule::command('backup:monitor')->dailyAt('16:30');
+6272 -5
View File
File diff suppressed because it is too large Load Diff