Actualización de modelos, controladores y middleware

This commit is contained in:
Lucho
2026-06-24 16:25:36 -03:00
parent ff2fa9b70f
commit 317d85b5c3
22 changed files with 494 additions and 125 deletions
+236 -10
View File
@@ -2,12 +2,15 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\AccionLog;
use App\Models\CredencialCliente; use App\Models\CredencialCliente;
use App\Models\CredencialProfesional; use App\Models\CredencialProfesional;
use App\Models\LogSeguridad;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class AuthController extends Controller class AuthController extends Controller
@@ -17,24 +20,65 @@ class AuthController extends Controller
$request->validate([ $request->validate([
'correo' => ['required', 'string'], 'correo' => ['required', 'string'],
'contra' => ['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')); $correo = trim((string) $request->input('correo'));
$contra = (string) $request->input('contra'); $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)) { if (!$credencial || !$this->credencialValida($contra, (string) $credencial->contra)) {
return back() return back()
->withInput($request->except('contra')) ->withInput($request->except('contra'))
->with('login_error', 'Usuario o contraseña incorrectos.'); ->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); $token = Str::random(64);
$credencial->token = $token; $credencial->token = $token;
$credencial->fecha_hora = now(); $credencial->fecha_hora = now();
$credencial->save(); $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 public function loginPersonalWeb(Request $request): RedirectResponse
@@ -42,24 +86,75 @@ class AuthController extends Controller
$request->validate([ $request->validate([
'usuario' => ['required', 'string'], 'usuario' => ['required', 'string'],
'contra' => ['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')); $usuario = trim((string) $request->input('usuario'));
$contra = (string) $request->input('contra'); $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)) { if (!$credencial || !$this->credencialValida($contra, (string) $credencial->contra)) {
return back() return back()
->withInput($request->except('contra')) ->withInput($request->except('contra'))
->with('login_error', 'Usuario o contraseña incorrectos.'); ->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); $token = Str::random(64);
$credencial->token = $token; $credencial->token = $token;
$credencial->fecha_hora = now(); $credencial->fecha_hora = now();
$credencial->save(); $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 public function loginCliente(Request $request): JsonResponse
@@ -72,7 +167,7 @@ class AuthController extends Controller
$correo = trim((string) $request->input('correo')); $correo = trim((string) $request->input('correo'));
$contra = (string) $request->input('contra'); $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)) { if (!$credencial || !$this->credencialValida($contra, (string) $credencial->contra)) {
return response()->json([ return response()->json([
'success' => false, 'success' => false,
@@ -80,11 +175,28 @@ class AuthController extends Controller
], 401); ], 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); $token = Str::random(64);
$credencial->token = $token; $credencial->token = $token;
$credencial->fecha_hora = now(); $credencial->fecha_hora = now();
$credencial->save(); $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([ return response()->json([
'success' => true, 'success' => true,
'data' => [ 'data' => [
@@ -106,7 +218,7 @@ class AuthController extends Controller
$usuario = trim((string) $request->input('usuario')); $usuario = trim((string) $request->input('usuario'));
$contra = (string) $request->input('contra'); $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)) { if (!$credencial || !$this->credencialValida($contra, (string) $credencial->contra)) {
return response()->json([ return response()->json([
'success' => false, 'success' => false,
@@ -114,11 +226,32 @@ class AuthController extends Controller
], 401); ], 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); $token = Str::random(64);
$credencial->token = $token; $credencial->token = $token;
$credencial->fecha_hora = now(); $credencial->fecha_hora = now();
$credencial->save(); $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([ return response()->json([
'success' => true, 'success' => true,
'data' => [ 'data' => [
@@ -147,13 +280,13 @@ class AuthController extends Controller
$tipoDetectado = null; $tipoDetectado = null;
if ($tipo === 'cliente') { if ($tipo === 'cliente') {
$credencial = CredencialCliente::where('correo', $identificador)->first(); $credencial = CredencialCliente::with('cliente')->where('correo', $identificador)->first();
$tipoDetectado = 'cliente'; $tipoDetectado = 'cliente';
} elseif ($tipo === 'profesional') { } elseif ($tipo === 'profesional') {
$credencial = CredencialProfesional::where('usuario', $identificador)->first(); $credencial = CredencialProfesional::where('usuario', $identificador)->first();
$tipoDetectado = 'personal'; $tipoDetectado = 'personal';
} else { } else {
$credencial = CredencialCliente::where('correo', $identificador)->first(); $credencial = CredencialCliente::with('cliente')->where('correo', $identificador)->first();
$tipoDetectado = $credencial ? 'cliente' : null; $tipoDetectado = $credencial ? 'cliente' : null;
if (!$credencial) { if (!$credencial) {
@@ -169,11 +302,52 @@ class AuthController extends Controller
], 401); ], 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); $token = Str::random(64);
$credencial->token = $token; $credencial->token = $token;
$credencial->fecha_hora = now(); $credencial->fecha_hora = now();
$credencial->save(); $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([ return response()->json([
'success' => true, 'success' => true,
'data' => [ 'data' => [
@@ -186,6 +360,11 @@ class AuthController extends Controller
], 200); ], 200);
} }
private function honeypotValido(Request $request): bool
{
return trim((string) $request->input('website', '')) === '';
}
public function logout(Request $request): JsonResponse public function logout(Request $request): JsonResponse
{ {
$token = (string) $request->input('token', ''); $token = (string) $request->input('token', '');
@@ -196,8 +375,16 @@ class AuthController extends Controller
], 422); ], 422);
} }
$credencialCliente = CredencialCliente::where('token', $token)->first(); $credencialCliente = CredencialCliente::with('cliente')->where('token', $token)->first();
if ($credencialCliente) { 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->token = null;
$credencialCliente->fecha_hora = now(); $credencialCliente->fecha_hora = now();
$credencialCliente->save(); $credencialCliente->save();
@@ -208,8 +395,20 @@ class AuthController extends Controller
], 200); ], 200);
} }
$credencialProfesional = CredencialProfesional::where('token', $token)->first(); $credencialProfesional = CredencialProfesional::with(['administrador', 'profesional'])->where('token', $token)->first();
if ($credencialProfesional) { 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->token = null;
$credencialProfesional->fecha_hora = now(); $credencialProfesional->fecha_hora = now();
$credencialProfesional->save(); $credencialProfesional->save();
@@ -234,4 +433,31 @@ class AuthController extends Controller
return Hash::check($contraIngresada, $contraGuardada); 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,
]);
}
} }
@@ -1,79 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Models\EstadoProfesional;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class EstadoProfesionalController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(): JsonResponse
{
$items = EstadoProfesional::all();
return response()->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);
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SecurityHeaders
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$entornoLocal = app()->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;
}
}
+2
View File
@@ -14,6 +14,8 @@ class Administrador extends Model
'persona_id', 'persona_id',
'dni', 'dni',
'correo', 'correo',
'pregunta_secreta_hash',
'respuesta_secreta_hash',
'credencialprofesional_id', 'credencialprofesional_id',
]; ];
+3 -3
View File
@@ -232,10 +232,10 @@ class Agenda extends Model
} }
$diasPreferidosIds = array_values(array_unique($diasPreferidosIds)); $diasPreferidosIds = array_values(array_unique($diasPreferidosIds));
// Toma la duración de turno de la agenda; si no existe, usa 40 minutos. // Toma la duración de turno de la agenda; si no existe, usa 30 minutos.
$duracionTurno = (int) (self::where('id', $agendaId)->value('duracionturno') ?? 40); $duracionTurno = (int) (self::where('id', $agendaId)->value('duracionturno') ?? 30);
if ($duracionTurno <= 0) { 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). // Define el punto de inicio de la búsqueda desde el día siguiente (para evitar asignar turnos inmediatos del día actual).
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class AsistenteSinRespuesta extends Model
{
public $timestamps = false;
protected $table = 'asistente_sin_respuesta';
protected $fillable = [
'consulta',
'revisado',
];
protected $casts = [
'revisado' => 'boolean',
'created_at' => 'datetime',
];
}
+1 -1
View File
@@ -11,7 +11,7 @@ class Baja extends Model
protected $table = 'bajas'; protected $table = 'bajas';
protected $fillable = [ protected $fillable = [
'motivo', 'descripcion',
]; ];
//tiene una //tiene una
+1 -1
View File
@@ -44,7 +44,7 @@ class Cliente extends Model
public function profesionales() 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') ->withPivot('estadorelacion')
->withTimestamps(); ->withTimestamps();
} }
+2
View File
@@ -15,6 +15,8 @@ class CredencialCliente extends Model
'correo', 'correo',
'token', 'token',
'fecha_hora', 'fecha_hora',
'reset_token',
'reset_expira_en',
]; ];
//tiene un //tiene un
+2
View File
@@ -16,6 +16,8 @@ class CredencialProfesional extends Model
'rol', 'rol',
'token', 'token',
'fecha_hora', 'fecha_hora',
'reset_token',
'reset_expira_en',
]; ];
public function profesional() public function profesional()
-1
View File
@@ -12,7 +12,6 @@ use HasFactory;
protected $table = 'diasdeatenciones'; protected $table = 'diasdeatenciones';
protected $fillable = [ protected $fillable = [
'descripcion',
'agenda_id', 'agenda_id',
'dia_id', 'dia_id',
]; ];
-22
View File
@@ -1,22 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class EstadoProfesional extends Model
{
use HasFactory;
protected $table = 'estadosprofesionales';
protected $fillable = [
'descripcion',
];
public function profesional()
{
return $this->hasMany(Profesional::class, 'estadoprofesional_id', 'id');
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class FaqAsistente extends Model
{
use HasFactory;
protected $table = 'faq_asistentes';
protected $fillable = [
'intencion',
'palabras_clave',
'respuesta',
'orden',
'activo',
];
protected $casts = [
'palabras_clave' => 'array',
'activo' => 'boolean',
];
}
+1
View File
@@ -15,6 +15,7 @@ class Formulario extends Model
'nombrecompleto', 'nombrecompleto',
'correo', 'correo',
'celular', 'celular',
'ip_origen',
'estado', 'estado',
'profesion_id', 'profesion_id',
'servicio_id', 'servicio_id',
+3
View File
@@ -10,6 +10,7 @@ class LogSeguridad extends Model
use HasFactory; use HasFactory;
protected $table = 'logseguridades'; protected $table = 'logseguridades';
public $timestamps = false;
protected $fillable = [ protected $fillable = [
'descripcion', 'descripcion',
@@ -18,6 +19,8 @@ class LogSeguridad extends Model
'rol', 'rol',
'persona_id', 'persona_id',
'accion_id', 'accion_id',
'accion_descripcion',
'responsable_nombre',
]; ];
//pertenece a //pertenece a
+21
View File
@@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Notificacion extends Model
{
use HasFactory;
protected $table = 'notificaciones';
protected $primaryKey = 'notificacion_id';
public $timestamps = false;
protected $fillable = [
'tipo',
'mensaje_inicio',
'mensaje_final',
];
}
+5
View File
@@ -20,4 +20,9 @@ class Profesion extends Model
{ {
return $this->hasMany(Profesional::class, 'profesion_id', 'id'); return $this->hasMany(Profesional::class, 'profesion_id', 'id');
} }
public function profesionalesAsociados()
{
return $this->belongsToMany(Profesional::class, 'profesionales_profesiones', 'profesion_id', 'profesional_id');
}
} }
+6 -7
View File
@@ -16,7 +16,6 @@ class Profesional extends Model
'correo', 'correo',
'dni', 'dni',
'credencialprofesional_id', 'credencialprofesional_id',
'estadoprofesional_id',
'persona_id', 'persona_id',
'baja_id', 'baja_id',
'profesion_id', 'profesion_id',
@@ -30,16 +29,16 @@ class Profesional extends Model
return $this->belongsTo(Profesion::class, 'profesion_id', 'id'); 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() public function credencialProfesional()
{ {
return $this->belongsTo(CredencialProfesional::class, 'credencialprofesional_id', 'id'); return $this->belongsTo(CredencialProfesional::class, 'credencialprofesional_id', 'id');
} }
public function estadoProfesional()
{
return $this->belongsTo(EstadoProfesional::class, 'estadoprofesional_id', 'id');
}
public function persona() public function persona()
{ {
return $this->belongsTo(Persona::class, 'persona_id'); return $this->belongsTo(Persona::class, 'persona_id');
@@ -86,7 +85,7 @@ class Profesional extends Model
public function clientes() 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') ->withPivot('estadorelacion')
->withTimestamps(); ->withTimestamps();
} }
+1
View File
@@ -15,6 +15,7 @@ class Servicio extends Model
'titulo', 'titulo',
'estado', 'estado',
'descripcion', 'descripcion',
'visibleenweb',
'contenidoweb_id', 'contenidoweb_id',
'profesion_id', 'profesion_id',
'foto_id', 'foto_id',
+1
View File
@@ -13,6 +13,7 @@ class Turno extends Model
protected $fillable = [ protected $fillable = [
'inicio', 'inicio',
'correo', 'correo',
'celular',
'nombrecompleto', 'nombrecompleto',
'descripcion', 'descripcion',
'cliente_id', 'cliente_id',
+110 -1
View File
@@ -2,7 +2,15 @@
namespace App\Providers; 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\ServiceProvider;
use Illuminate\Support\Facades\View;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {
@@ -19,6 +27,107 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function boot(): void 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);
});
} }
} }