From 317d85b5c3205fba62996007b0e221ef8669fe70 Mon Sep 17 00:00:00 2001 From: Lucho Date: Wed, 24 Jun 2026 16:25:36 -0300 Subject: [PATCH] =?UTF-8?q?Actualizaci=C3=B3n=20de=20modelos,=20controlado?= =?UTF-8?q?res=20y=20middleware?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/AuthController.php | 246 +++++++++++++++++- .../EstadoProfesionalController.php | 79 ------ app/Http/Middleware/EnsureSingleSession.php | 0 app/Http/Middleware/SecurityHeaders.php | 51 ++++ app/Models/Administrador.php | 2 + app/Models/Agenda.php | 6 +- app/Models/AsistenteSinRespuesta.php | 22 ++ app/Models/Baja.php | 2 +- app/Models/Cliente.php | 2 +- app/Models/CredencialCliente.php | 2 + app/Models/CredencialProfesional.php | 2 + app/Models/DiaDeAtencion.php | 1 - app/Models/EstadoProfesional.php | 22 -- app/Models/FaqAsistente.php | 26 ++ app/Models/Formulario.php | 1 + app/Models/LogSeguridad.php | 3 + app/Models/Notificacion.php | 21 ++ app/Models/Profesion.php | 5 + app/Models/Profesional.php | 13 +- app/Models/Servicio.php | 1 + app/Models/Turno.php | 1 + app/Providers/AppServiceProvider.php | 111 +++++++- 22 files changed, 494 insertions(+), 125 deletions(-) delete mode 100644 app/Http/Controllers/EstadoProfesionalController.php create mode 100644 app/Http/Middleware/EnsureSingleSession.php create mode 100644 app/Http/Middleware/SecurityHeaders.php create mode 100644 app/Models/AsistenteSinRespuesta.php delete mode 100644 app/Models/EstadoProfesional.php create mode 100644 app/Models/FaqAsistente.php create mode 100644 app/Models/Notificacion.php diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index 888fa52..013f09b 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -2,12 +2,15 @@ namespace App\Http\Controllers; +use App\Models\AccionLog; use App\Models\CredencialCliente; use App\Models\CredencialProfesional; +use App\Models\LogSeguridad; use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Str; class AuthController extends Controller @@ -17,24 +20,65 @@ class AuthController extends Controller $request->validate([ 'correo' => ['required', 'string'], 'contra' => ['required', 'string'], + 'website' => ['nullable', 'string', 'max:255'], ]); + if (!$this->honeypotValido($request)) { + return back() + ->withInput($request->except(['contra', 'website'])) + ->withErrors(['login_error' => 'No se pudo procesar el inicio de sesión.']); + } + $correo = trim((string) $request->input('correo')); $contra = (string) $request->input('contra'); - $credencial = CredencialCliente::where('correo', $correo)->first(); + $credencial = CredencialCliente::with('cliente.persona.telefonos')->where('correo', $correo)->first(); if (!$credencial || !$this->credencialValida($contra, (string) $credencial->contra)) { return back() ->withInput($request->except('contra')) ->with('login_error', 'Usuario o contraseña incorrectos.'); } + if ((int) ($credencial->cliente?->baja_id ?? 0) !== 1) { + return back() + ->withInput($request->except('contra')) + ->with('login_error', 'No es posible iniciar sesion. Comuníquese con un profesional de Abogadas del Litoral'); + } + $token = Str::random(64); $credencial->token = $token; $credencial->fecha_hora = now(); $credencial->save(); - return back()->with('login_success', 'Login exitoso.'); + $personaCliente = $credencial->cliente?->persona; + $nombreCliente = trim((string) ($personaCliente?->nombre ?? '')); + $apellidoCliente = trim((string) ($personaCliente?->apellido ?? '')); + $celularCliente = trim((string) ($personaCliente?->telefonos?->first()?->telefono ?? '')); + + $request->session()->regenerate(); + $request->session()->regenerateToken(); + + $request->session()->put([ + 'cliente_auth' => true, + 'cliente_id' => (int) ($credencial->cliente?->id ?? 0), + 'cliente_token' => $token, + 'cliente_correo' => (string) $credencial->correo, + 'cliente_nombre' => $nombreCliente !== '' ? $nombreCliente : (string) $credencial->correo, + 'cliente_apellido' => $apellidoCliente, + 'cliente_celular' => $celularCliente, + ]); + + $this->registrarAccionSeguridad( + (int) ($credencial->cliente?->persona_id ?? 0), + 'CLIENTE', + (string) $request->ip(), + 'Inició sesión', + 'El cliente ID ' . (int) ($credencial->cliente?->id ?? 0) . ' inició sesión.' + ); + + $this->limpiarThrottle($request, 'login-cliente-web'); + + return redirect('/cliente/dashboard')->with('login_success', 'Login exitoso.'); } public function loginPersonalWeb(Request $request): RedirectResponse @@ -42,24 +86,75 @@ class AuthController extends Controller $request->validate([ 'usuario' => ['required', 'string'], 'contra' => ['required', 'string'], + 'website' => ['nullable', 'string', 'max:255'], ]); + if (!$this->honeypotValido($request)) { + return back() + ->withInput($request->except(['contra', 'website'])) + ->withErrors(['login_error' => 'No se pudo procesar el inicio de sesión.']); + } + $usuario = trim((string) $request->input('usuario')); $contra = (string) $request->input('contra'); - $credencial = CredencialProfesional::where('usuario', $usuario)->first(); + $credencial = CredencialProfesional::with(['administrador.persona', 'profesional.persona']) + ->where('usuario', $usuario) + ->first(); if (!$credencial || !$this->credencialValida($contra, (string) $credencial->contra)) { return back() ->withInput($request->except('contra')) ->with('login_error', 'Usuario o contraseña incorrectos.'); } + if (strtoupper((string) $credencial->rol) !== 'ADMIN' && (int) ($credencial->profesional?->baja_id ?? 0) !== 1) { + return back() + ->withInput($request->except('contra')) + ->with('login_error', 'Usted está dado de baja. Comuníquese con el administrador'); + } + $token = Str::random(64); $credencial->token = $token; $credencial->fecha_hora = now(); $credencial->save(); - return back()->with('login_success', 'Login exitoso.'); + $rol = strtoupper((string) $credencial->rol); + $personaProfesional = $credencial->profesional?->persona; + $nombreProfesional = trim((string) ($personaProfesional?->nombre ?? '') . ' ' . (string) ($personaProfesional?->apellido ?? '')); + $personaAdmin = $credencial->administrador?->persona; + $nombreAdmin = trim((string) ($personaAdmin?->nombre ?? '') . ' ' . (string) ($personaAdmin?->apellido ?? '')); + $nombreSesion = $rol === 'ADMIN' ? $nombreAdmin : $nombreProfesional; + + $request->session()->regenerate(); + $request->session()->regenerateToken(); + + $request->session()->put([ + 'personal_auth' => true, + 'personal_token' => $token, + 'personal_usuario' => (string) $credencial->usuario, + 'personal_nombre' => $nombreSesion !== '' ? $nombreSesion : (string) $credencial->usuario, + 'personal_apellido' => $rol === 'ADMIN' + ? ($personaAdmin?->apellido ?? '') + : ($personaProfesional?->apellido ?? ''), + 'personal_rol' => $rol, + 'personal_credencial_id' => (int) $credencial->id, + ]); + + $this->registrarAccionSeguridad( + $rol === 'ADMIN' + ? (int) ($credencial->administrador?->persona_id ?? 0) + : (int) ($credencial->profesional?->persona_id ?? 0), + $rol === 'ADMIN' ? 'ADMIN' : 'PROFESIONAL', + (string) $request->ip(), + 'Inició sesión', + ($rol === 'ADMIN' ? 'La administradora ID ' . (int) ($credencial->administrador?->id ?? 0) : 'El profesional ID ' . (int) ($credencial->profesional?->id ?? 0)) . ' inició sesión.' + ); + + $this->limpiarThrottle($request, 'login-personal-web'); + + $redirectPath = $rol === 'ADMIN' ? '/administrador/dashboard' : '/profesional/dashboard'; + + return redirect($redirectPath)->with('login_success', 'Login exitoso.'); } public function loginCliente(Request $request): JsonResponse @@ -72,7 +167,7 @@ class AuthController extends Controller $correo = trim((string) $request->input('correo')); $contra = (string) $request->input('contra'); - $credencial = CredencialCliente::where('correo', $correo)->first(); + $credencial = CredencialCliente::with('cliente')->where('correo', $correo)->first(); if (!$credencial || !$this->credencialValida($contra, (string) $credencial->contra)) { return response()->json([ 'success' => false, @@ -80,11 +175,28 @@ class AuthController extends Controller ], 401); } + if ((int) ($credencial->cliente?->baja_id ?? 0) !== 1) { + return response()->json([ + 'success' => false, + 'message' => 'No es posible iniciar sesion. Comuníquese con un profesional de Abogadas del Litoral', + ], 403); + } + $token = Str::random(64); $credencial->token = $token; $credencial->fecha_hora = now(); $credencial->save(); + $this->registrarAccionSeguridad( + (int) ($credencial->cliente?->persona_id ?? 0), + 'CLIENTE', + (string) $request->ip(), + 'Inició sesión', + 'El cliente ID ' . (int) ($credencial->cliente?->id ?? 0) . ' inició sesión.' + ); + + $this->limpiarThrottle($request, 'login-cliente-api'); + return response()->json([ 'success' => true, 'data' => [ @@ -106,7 +218,7 @@ class AuthController extends Controller $usuario = trim((string) $request->input('usuario')); $contra = (string) $request->input('contra'); - $credencial = CredencialProfesional::where('usuario', $usuario)->first(); + $credencial = CredencialProfesional::with(['administrador', 'profesional'])->where('usuario', $usuario)->first(); if (!$credencial || !$this->credencialValida($contra, (string) $credencial->contra)) { return response()->json([ 'success' => false, @@ -114,11 +226,32 @@ class AuthController extends Controller ], 401); } + if (strtoupper((string) $credencial->rol) !== 'ADMIN' && (int) ($credencial->profesional?->baja_id ?? 0) !== 1) { + return response()->json([ + 'success' => false, + 'message' => 'Usted está dado de baja. Comuníquese con el administrador', + ], 403); + } + $token = Str::random(64); $credencial->token = $token; $credencial->fecha_hora = now(); $credencial->save(); + $rol = strtoupper((string) $credencial->rol); + + $this->registrarAccionSeguridad( + $rol === 'ADMIN' + ? (int) ($credencial->administrador?->persona_id ?? 0) + : (int) ($credencial->profesional?->persona_id ?? 0), + $rol === 'ADMIN' ? 'ADMIN' : 'PROFESIONAL', + (string) $request->ip(), + 'Inició sesión', + ($rol === 'ADMIN' ? 'La administradora ID ' . (int) ($credencial->administrador?->id ?? 0) : 'El profesional ID ' . (int) ($credencial->profesional?->id ?? 0)) . ' inició sesión.' + ); + + $this->limpiarThrottle($request, 'login-personal-api'); + return response()->json([ 'success' => true, 'data' => [ @@ -147,13 +280,13 @@ class AuthController extends Controller $tipoDetectado = null; if ($tipo === 'cliente') { - $credencial = CredencialCliente::where('correo', $identificador)->first(); + $credencial = CredencialCliente::with('cliente')->where('correo', $identificador)->first(); $tipoDetectado = 'cliente'; } elseif ($tipo === 'profesional') { $credencial = CredencialProfesional::where('usuario', $identificador)->first(); $tipoDetectado = 'personal'; } else { - $credencial = CredencialCliente::where('correo', $identificador)->first(); + $credencial = CredencialCliente::with('cliente')->where('correo', $identificador)->first(); $tipoDetectado = $credencial ? 'cliente' : null; if (!$credencial) { @@ -169,11 +302,52 @@ class AuthController extends Controller ], 401); } + if ($credencial instanceof CredencialCliente + && (int) ($credencial->cliente?->baja_id ?? 0) !== 1) { + return response()->json([ + 'success' => false, + 'message' => 'No es posible iniciar sesion. Comuníquese con un profesional de Abogadas del Litoral', + ], 403); + } + + if ($credencial instanceof CredencialProfesional + && strtoupper((string) $credencial->rol) !== 'ADMIN' + && (int) ($credencial->profesional?->baja_id ?? 0) !== 1) { + return response()->json([ + 'success' => false, + 'message' => 'Usted está dado de baja. Comuníquese con el administrador', + ], 403); + } + $token = Str::random(64); $credencial->token = $token; $credencial->fecha_hora = now(); $credencial->save(); + if ($credencial instanceof CredencialCliente) { + $this->registrarAccionSeguridad( + (int) ($credencial->cliente?->persona_id ?? 0), + 'CLIENTE', + (string) $request->ip(), + 'Inició sesión', + 'El cliente ID ' . (int) ($credencial->cliente?->id ?? 0) . ' inició sesión.' + ); + } elseif ($credencial instanceof CredencialProfesional) { + $rolDescripcion = strtoupper((string) $credencial->rol) === 'ADMIN' ? 'ADMIN' : 'PROFESIONAL'; + + $this->registrarAccionSeguridad( + $rolDescripcion === 'ADMIN' + ? (int) ($credencial->administrador?->persona_id ?? 0) + : (int) ($credencial->profesional?->persona_id ?? 0), + $rolDescripcion, + (string) $request->ip(), + 'Inició sesión', + ($rolDescripcion === 'ADMIN' ? 'La administradora ID ' . (int) ($credencial->administrador?->id ?? 0) : 'El profesional ID ' . (int) ($credencial->profesional?->id ?? 0)) . ' inició sesión.' + ); + } + + $this->limpiarThrottle($request, 'login-api-general'); + return response()->json([ 'success' => true, 'data' => [ @@ -186,6 +360,11 @@ class AuthController extends Controller ], 200); } + private function honeypotValido(Request $request): bool + { + return trim((string) $request->input('website', '')) === ''; + } + public function logout(Request $request): JsonResponse { $token = (string) $request->input('token', ''); @@ -196,8 +375,16 @@ class AuthController extends Controller ], 422); } - $credencialCliente = CredencialCliente::where('token', $token)->first(); + $credencialCliente = CredencialCliente::with('cliente')->where('token', $token)->first(); if ($credencialCliente) { + $this->registrarAccionSeguridad( + (int) ($credencialCliente->cliente?->persona_id ?? 0), + 'CLIENTE', + (string) $request->ip(), + 'Cerró sesión', + 'El cliente ID ' . (int) ($credencialCliente->cliente?->id ?? 0) . ' cerró sesión.' + ); + $credencialCliente->token = null; $credencialCliente->fecha_hora = now(); $credencialCliente->save(); @@ -208,8 +395,20 @@ class AuthController extends Controller ], 200); } - $credencialProfesional = CredencialProfesional::where('token', $token)->first(); + $credencialProfesional = CredencialProfesional::with(['administrador', 'profesional'])->where('token', $token)->first(); if ($credencialProfesional) { + $rol = strtoupper((string) $credencialProfesional->rol); + + $this->registrarAccionSeguridad( + $rol === 'ADMIN' + ? (int) ($credencialProfesional->administrador?->persona_id ?? 0) + : (int) ($credencialProfesional->profesional?->persona_id ?? 0), + $rol === 'ADMIN' ? 'ADMIN' : 'PROFESIONAL', + (string) $request->ip(), + 'Cerró sesión', + ($rol === 'ADMIN' ? 'La administradora ID ' . (int) ($credencialProfesional->administrador?->id ?? 0) : 'El profesional ID ' . (int) ($credencialProfesional->profesional?->id ?? 0)) . ' cerró sesión.' + ); + $credencialProfesional->token = null; $credencialProfesional->fecha_hora = now(); $credencialProfesional->save(); @@ -234,4 +433,31 @@ class AuthController extends Controller return Hash::check($contraIngresada, $contraGuardada); } + + private function limpiarThrottle(Request $request, string $prefijo): void + { + RateLimiter::clear(md5($prefijo . $request->ip())); + } + + private function registrarAccionSeguridad(?int $personaId, string $rol, string $ipOrigen, string $accionDescripcion, string $descripcion): void + { + if (!$personaId) { + return; + } + + $accion = AccionLog::query()->firstWhere('descripcion', $accionDescripcion); + + if (!$accion) { + return; + } + + LogSeguridad::create([ + 'descripcion' => $descripcion, + 'fechahora' => now(), + 'IPorigen' => $ipOrigen, + 'rol' => $rol, + 'persona_id' => (int) $personaId, + 'accion_id' => (int) $accion->id, + ]); + } } diff --git a/app/Http/Controllers/EstadoProfesionalController.php b/app/Http/Controllers/EstadoProfesionalController.php deleted file mode 100644 index 8c7c3e6..0000000 --- a/app/Http/Controllers/EstadoProfesionalController.php +++ /dev/null @@ -1,79 +0,0 @@ -json([ - 'success' => true, - 'data' => $items, - 'message' => 'Registros obtenidos correctamente', - ], 200); - } - - /** - * Store a newly created resource in storage. - */ - public function store(Request $request): JsonResponse - { - $payload = $request->only((new EstadoProfesional())->getFillable()); - $estadoProfesional = EstadoProfesional::create($payload); - - return response()->json([ - 'success' => true, - 'data' => $estadoProfesional, - 'message' => 'Registro creado correctamente', - ], 201); - } - - /** - * Display the specified resource. - */ - public function show(EstadoProfesional $estadoProfesional): JsonResponse - { - return response()->json([ - 'success' => true, - 'data' => $estadoProfesional, - 'message' => 'Registro obtenido correctamente', - ], 200); - } - - /** - * Update the specified resource in storage. - */ - public function update(Request $request, EstadoProfesional $estadoProfesional): JsonResponse - { - $payload = $request->only((new EstadoProfesional())->getFillable()); - $estadoProfesional->update($payload); - - return response()->json([ - 'success' => true, - 'data' => $estadoProfesional, - 'message' => 'Registro actualizado correctamente', - ], 200); - } - - /** - * Remove the specified resource from storage. - */ - public function destroy(EstadoProfesional $estadoProfesional): JsonResponse - { - $estadoProfesional->delete(); - - return response()->json([ - 'success' => true, - 'message' => 'Registro eliminado correctamente', - ], 200); - } -} \ No newline at end of file diff --git a/app/Http/Middleware/EnsureSingleSession.php b/app/Http/Middleware/EnsureSingleSession.php new file mode 100644 index 0000000..e69de29 diff --git a/app/Http/Middleware/SecurityHeaders.php b/app/Http/Middleware/SecurityHeaders.php new file mode 100644 index 0000000..0136f58 --- /dev/null +++ b/app/Http/Middleware/SecurityHeaders.php @@ -0,0 +1,51 @@ +environment(['local', 'development']); + $sesionAutenticada = (bool) $request->session()->get('cliente_auth', false) + || (bool) $request->session()->get('personal_auth', false); + + $styleSrc = $entornoLocal + ? "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net http:;" + : "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;"; + + $scriptSrc = $entornoLocal + ? "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net http:;" + : "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;"; + + $connectSrc = $entornoLocal + ? "connect-src 'self' http: https: ws: wss:;" + : "connect-src 'self';"; + + $response->headers->set('X-Frame-Options', 'SAMEORIGIN'); + $response->headers->set('X-Content-Type-Options', 'nosniff'); + $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin'); + $response->headers->set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()'); + $response->headers->set( + 'Content-Security-Policy', + "default-src 'self'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'; img-src 'self' data: https:; {$styleSrc} {$scriptSrc} font-src 'self' data: https:; {$connectSrc} frame-src 'self' https://maps.google.com https://www.google.com;" + ); + + if ($request->isSecure()) { + $response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); + } + + if ($sesionAutenticada) { + $response->headers->set('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0, private'); + $response->headers->set('Pragma', 'no-cache'); + $response->headers->set('Expires', 'Thu, 01 Jan 1970 00:00:00 GMT'); + } + + return $response; + } +} diff --git a/app/Models/Administrador.php b/app/Models/Administrador.php index 8edab09..98c4be1 100644 --- a/app/Models/Administrador.php +++ b/app/Models/Administrador.php @@ -14,6 +14,8 @@ class Administrador extends Model 'persona_id', 'dni', 'correo', + 'pregunta_secreta_hash', + 'respuesta_secreta_hash', 'credencialprofesional_id', ]; diff --git a/app/Models/Agenda.php b/app/Models/Agenda.php index 2b378ec..72b5a43 100644 --- a/app/Models/Agenda.php +++ b/app/Models/Agenda.php @@ -232,10 +232,10 @@ class Agenda extends Model } $diasPreferidosIds = array_values(array_unique($diasPreferidosIds)); - // Toma la duración de turno de la agenda; si no existe, usa 40 minutos. - $duracionTurno = (int) (self::where('id', $agendaId)->value('duracionturno') ?? 40); + // Toma la duración de turno de la agenda; si no existe, usa 30 minutos. + $duracionTurno = (int) (self::where('id', $agendaId)->value('duracionturno') ?? 30); if ($duracionTurno <= 0) { - $duracionTurno = 40; + $duracionTurno = 30; } // Define el punto de inicio de la búsqueda desde el día siguiente (para evitar asignar turnos inmediatos del día actual). diff --git a/app/Models/AsistenteSinRespuesta.php b/app/Models/AsistenteSinRespuesta.php new file mode 100644 index 0000000..b435ce3 --- /dev/null +++ b/app/Models/AsistenteSinRespuesta.php @@ -0,0 +1,22 @@ + 'boolean', + 'created_at' => 'datetime', + ]; +} diff --git a/app/Models/Baja.php b/app/Models/Baja.php index 8ac69b1..1e5feae 100644 --- a/app/Models/Baja.php +++ b/app/Models/Baja.php @@ -11,7 +11,7 @@ class Baja extends Model protected $table = 'bajas'; protected $fillable = [ - 'motivo', + 'descripcion', ]; //tiene una diff --git a/app/Models/Cliente.php b/app/Models/Cliente.php index 9346722..c9be585 100644 --- a/app/Models/Cliente.php +++ b/app/Models/Cliente.php @@ -44,7 +44,7 @@ class Cliente extends Model public function profesionales() { - return $this->belongsToMany(Profesional::class, 'profesional_cliente','cliente_id', 'profesional_id') + return $this->belongsToMany(Profesional::class, 'profesionales_clientes','cliente_id', 'profesional_id') ->withPivot('estadorelacion') ->withTimestamps(); } diff --git a/app/Models/CredencialCliente.php b/app/Models/CredencialCliente.php index 39711dd..6dff0a8 100644 --- a/app/Models/CredencialCliente.php +++ b/app/Models/CredencialCliente.php @@ -15,6 +15,8 @@ class CredencialCliente extends Model 'correo', 'token', 'fecha_hora', + 'reset_token', + 'reset_expira_en', ]; //tiene un diff --git a/app/Models/CredencialProfesional.php b/app/Models/CredencialProfesional.php index 9511b7a..219c217 100644 --- a/app/Models/CredencialProfesional.php +++ b/app/Models/CredencialProfesional.php @@ -16,6 +16,8 @@ class CredencialProfesional extends Model 'rol', 'token', 'fecha_hora', + 'reset_token', + 'reset_expira_en', ]; public function profesional() diff --git a/app/Models/DiaDeAtencion.php b/app/Models/DiaDeAtencion.php index 0970568..ebcb359 100644 --- a/app/Models/DiaDeAtencion.php +++ b/app/Models/DiaDeAtencion.php @@ -12,7 +12,6 @@ use HasFactory; protected $table = 'diasdeatenciones'; protected $fillable = [ - 'descripcion', 'agenda_id', 'dia_id', ]; diff --git a/app/Models/EstadoProfesional.php b/app/Models/EstadoProfesional.php deleted file mode 100644 index 9ff4099..0000000 --- a/app/Models/EstadoProfesional.php +++ /dev/null @@ -1,22 +0,0 @@ -hasMany(Profesional::class, 'estadoprofesional_id', 'id'); - } - -} diff --git a/app/Models/FaqAsistente.php b/app/Models/FaqAsistente.php new file mode 100644 index 0000000..f5381cf --- /dev/null +++ b/app/Models/FaqAsistente.php @@ -0,0 +1,26 @@ + 'array', + 'activo' => 'boolean', + ]; +} diff --git a/app/Models/Formulario.php b/app/Models/Formulario.php index f695399..1b66a7a 100644 --- a/app/Models/Formulario.php +++ b/app/Models/Formulario.php @@ -15,6 +15,7 @@ class Formulario extends Model 'nombrecompleto', 'correo', 'celular', + 'ip_origen', 'estado', 'profesion_id', 'servicio_id', diff --git a/app/Models/LogSeguridad.php b/app/Models/LogSeguridad.php index 033cbaa..0de66e4 100644 --- a/app/Models/LogSeguridad.php +++ b/app/Models/LogSeguridad.php @@ -10,6 +10,7 @@ class LogSeguridad extends Model use HasFactory; protected $table = 'logseguridades'; + public $timestamps = false; protected $fillable = [ 'descripcion', @@ -18,6 +19,8 @@ class LogSeguridad extends Model 'rol', 'persona_id', 'accion_id', + 'accion_descripcion', + 'responsable_nombre', ]; //pertenece a diff --git a/app/Models/Notificacion.php b/app/Models/Notificacion.php new file mode 100644 index 0000000..f76688a --- /dev/null +++ b/app/Models/Notificacion.php @@ -0,0 +1,21 @@ +hasMany(Profesional::class, 'profesion_id', 'id'); } + + public function profesionalesAsociados() + { + return $this->belongsToMany(Profesional::class, 'profesionales_profesiones', 'profesion_id', 'profesional_id'); + } } diff --git a/app/Models/Profesional.php b/app/Models/Profesional.php index d22f8a7..2825589 100644 --- a/app/Models/Profesional.php +++ b/app/Models/Profesional.php @@ -16,7 +16,6 @@ class Profesional extends Model 'correo', 'dni', 'credencialprofesional_id', - 'estadoprofesional_id', 'persona_id', 'baja_id', 'profesion_id', @@ -30,16 +29,16 @@ class Profesional extends Model return $this->belongsTo(Profesion::class, 'profesion_id', 'id'); } + public function profesiones() + { + return $this->belongsToMany(Profesion::class, 'profesionales_profesiones', 'profesional_id', 'profesion_id'); + } + public function credencialProfesional() { return $this->belongsTo(CredencialProfesional::class, 'credencialprofesional_id', 'id'); } - public function estadoProfesional() - { - return $this->belongsTo(EstadoProfesional::class, 'estadoprofesional_id', 'id'); - } - public function persona() { return $this->belongsTo(Persona::class, 'persona_id'); @@ -86,7 +85,7 @@ class Profesional extends Model public function clientes() { - return $this->belongsToMany(Cliente::class, 'profesionales_cliente', 'profesional_id', 'cliente_id') + return $this->belongsToMany(Cliente::class, 'profesionales_clientes', 'profesional_id', 'cliente_id') ->withPivot('estadorelacion') ->withTimestamps(); } diff --git a/app/Models/Servicio.php b/app/Models/Servicio.php index a92cf6e..eae5c56 100644 --- a/app/Models/Servicio.php +++ b/app/Models/Servicio.php @@ -15,6 +15,7 @@ class Servicio extends Model 'titulo', 'estado', 'descripcion', + 'visibleenweb', 'contenidoweb_id', 'profesion_id', 'foto_id', diff --git a/app/Models/Turno.php b/app/Models/Turno.php index 51a81c0..907ec1f 100644 --- a/app/Models/Turno.php +++ b/app/Models/Turno.php @@ -13,6 +13,7 @@ class Turno extends Model protected $fillable = [ 'inicio', 'correo', + 'celular', 'nombrecompleto', 'descripcion', 'cliente_id', diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b6..a1160fa 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,7 +2,15 @@ namespace App\Providers; +use App\Models\Bug; +use App\Models\Profesional; +use Illuminate\Cache\RateLimiting\Limit; +use Illuminate\Http\Request; +use Illuminate\Pagination\Paginator; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\ServiceProvider; +use Illuminate\Support\Facades\View; class AppServiceProvider extends ServiceProvider { @@ -19,6 +27,107 @@ class AppServiceProvider extends ServiceProvider */ public function boot(): void { - // + Paginator::useBootstrapFive(); + + RateLimiter::for('login-cliente-web', fn (Request $request) => Limit::perHour(5)->by($request->ip())); + RateLimiter::for('login-personal-web', fn (Request $request) => Limit::perHour(5)->by($request->ip())); + RateLimiter::for('login-cliente-api', fn (Request $request) => Limit::perHour(5)->by($request->ip())); + RateLimiter::for('login-personal-api', fn (Request $request) => Limit::perHour(5)->by($request->ip())); + RateLimiter::for('login-api-general', fn (Request $request) => Limit::perHour(5)->by($request->ip())); + RateLimiter::for('recuperar-cliente', fn (Request $request) => Limit::perHour(5)->by($request->ip())); + RateLimiter::for('recuperar-personal', fn (Request $request) => Limit::perHour(5)->by($request->ip())); + RateLimiter::for('recuperar-admin', fn (Request $request) => Limit::perHour(5)->by($request->ip())); + RateLimiter::for('recuperar-admin-pregunta', fn (Request $request) => Limit::perHour(5)->by($request->ip())); + RateLimiter::for('reportar-bugs', fn (Request $request) => Limit::perHour(5)->by($request->ip())); + RateLimiter::for('asistente-chat', fn (Request $request) => Limit::perMinute(20)->by($request->ip())); + + View::composer('profesional.*', function ($view): void { + $credencialId = (int) session('personal_credencial_id', 0); + $profesionalId = 0; + + if ($credencialId > 0) { + $profesionalId = (int) Profesional::query() + ->where('credencialprofesional_id', $credencialId) + ->value('id'); + } + + $formulariosPendientesCount = 0; + + if ($profesionalId > 0) { + $formulariosPendientesCount = DB::table('profesionales_formularios as pf') + ->join('formularios as f', 'f.id', '=', 'pf.formulario_id') + ->where('pf.profesional_id', $profesionalId) + ->whereRaw("LOWER(TRIM(pf.estado)) = 'pendiente'") + ->whereRaw("LOWER(TRIM(f.estado)) = 'pendiente'") + ->count(); + } + + $view->with('formulariosPendientesCount', $formulariosPendientesCount); + + $notificacionesClaves = []; + if ($profesionalId > 0) { + $hoy = now()->toDateString(); + $manana = now()->addDay()->toDateString(); + + $turnosHoy = DB::table('turnos as t') + ->where('t.profesional_id', $profesionalId) + ->whereDate('t.inicio', $hoy) + ->select('t.nombrecompleto', 't.inicio') + ->get(); + + foreach ($turnosHoy as $turno) { + $nombre = trim((string) ($turno->nombrecompleto ?? 'Sin nombre')); + $hora = $turno->inicio ? \Illuminate\Support\Carbon::parse((string) $turno->inicio)->format('H:i') : '-'; + $titulo = 'Turno hoy a las ' . $hora . ' — ' . $nombre; + $fecha = 'Hoy ' . $hora; + $notificacionesClaves[] = base64_encode('turno_hoy|' . $titulo . '|' . $fecha); + } + + $turnosManana = DB::table('turnos as t') + ->where('t.profesional_id', $profesionalId) + ->whereDate('t.inicio', $manana) + ->select('t.nombrecompleto', 't.inicio') + ->get(); + + foreach ($turnosManana as $turno) { + $nombre = trim((string) ($turno->nombrecompleto ?? 'Sin nombre')); + $hora = $turno->inicio ? \Illuminate\Support\Carbon::parse((string) $turno->inicio)->format('H:i') : '-'; + $titulo = 'Turno mañana a las ' . $hora . ' — ' . $nombre; + $fecha = 'Mañana ' . $hora; + $notificacionesClaves[] = base64_encode('turno_manana|' . $titulo . '|' . $fecha); + } + + $estadoCanceladoId = (int) DB::table('estadosturnos')->whereRaw('LOWER(TRIM(descripcion)) = ?', ['cancelado'])->value('id'); + if ($estadoCanceladoId > 0) { + $turnosCancelados = DB::table('turnos as t') + ->where('t.profesional_id', $profesionalId) + ->where('t.estadoturno_id', $estadoCanceladoId) + ->where('t.updated_at', '>=', now()->subDays(7)) + ->select('t.nombrecompleto', 't.inicio', 't.updated_at') + ->get(); + + foreach ($turnosCancelados as $turno) { + $nombre = trim((string) ($turno->nombrecompleto ?? 'Sin nombre')); + $fecha = $turno->updated_at + ? \Illuminate\Support\Carbon::parse((string) $turno->updated_at)->format('d/m/Y') + : '-'; + $titulo = 'Turno cancelado — ' . $nombre; + $notificacionesClaves[] = base64_encode('turno_cancelado|' . $titulo . '|' . $fecha); + } + } + } + + $notificacionesCount = count($notificacionesClaves); + $view->with('notificacionesCount', $notificacionesCount); + $view->with('notificacionesClaves', $notificacionesClaves); + }); + + View::composer('administrador.*', function ($view): void { + $bugsPendientesCount = Bug::query() + ->whereRaw("LOWER(TRIM(estado)) = 'pendiente'") + ->count(); + + $view->with('bugsPendientesCount', $bugsPendientesCount); + }); } }