Primer version funcional

This commit is contained in:
Administrador Ibiza
2025-12-29 23:37:11 -06:00
commit 5289fd4133
294 changed files with 111418 additions and 0 deletions

133
core/ApiAuth.php Executable file
View File

@@ -0,0 +1,133 @@
<?php
class ApiAuth {
public static function authenticate() {
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? '';
$customHeader = $_SERVER['HTTP_X_AUTH_TOKEN'] ?? '';
// 1. Intentar JWT primero (para móvil) - header estándar
if (preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
require_once __DIR__ . '/JWT.php';
$token = $matches[1];
$decoded = JWT::decode($token);
if ($decoded) {
return $decoded;
}
http_response_code(401);
echo json_encode([
'success' => false,
'message' => 'Token inválido o expirado'
]);
exit;
}
// 2. Intentar JWT con header personalizado X-Auth-Token (fallback)
if (!empty($customHeader)) {
require_once __DIR__ . '/JWT.php';
$decoded = JWT::decode($customHeader);
if ($decoded) {
return $decoded;
}
http_response_code(401);
echo json_encode([
'success' => false,
'message' => 'Token inválido o expirado'
]);
exit;
}
// 3. Fallback a sesión PHP (para web)
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (isset($_SESSION['user_id'])) {
return [
'user_id' => $_SESSION['user_id'],
'username' => $_SESSION['username'],
'role' => $_SESSION['role'],
'first_name' => $_SESSION['first_name'] ?? '',
'last_name' => $_SESSION['last_name'] ?? ''
];
}
// 4. Sin autenticación
http_response_code(401);
echo json_encode([
'success' => false,
'message' => 'No autenticado'
]);
exit;
}
public static function requireAdmin() {
$user = self::authenticate();
if ($user['role'] !== 'ADMIN') {
http_response_code(403);
echo json_encode([
'success' => false,
'message' => 'Se requiere rol de Administrador'
]);
exit;
}
return $user;
}
public static function requireCapturist() {
$user = self::authenticate();
if ($user['role'] !== 'ADMIN' && $user['role'] !== 'CAPTURIST') {
http_response_code(403);
echo json_encode([
'success' => false,
'message' => 'Se requiere rol de Capturista o Administrador'
]);
exit;
}
return $user;
}
public static function getAccessibleHouseIds() {
$user = self::authenticate();
if ($user['role'] === 'ADMIN') {
require_once __DIR__ . '/../models/House.php';
$db = Database::getInstance();
$result = $db->fetchAll("SELECT id FROM houses");
return array_column($result, 'id');
}
if ($user['role'] === 'LECTOR') {
require_once __DIR__ . '/../models/UserPermission.php';
return UserPermission::getUserHouseIds($user['user_id']);
}
// VIEWER, CAPTURIST
require_once __DIR__ . '/../models/House.php';
$db = Database::getInstance();
$result = $db->fetchAll("SELECT id FROM houses");
return array_column($result, 'id');
}
public static function isLector() {
$user = self::authenticate();
return $user['role'] === 'LECTOR';
}
public static function getUserId() {
$user = self::authenticate();
return $user['user_id'];
}
public static function getRole() {
$user = self::authenticate();
return $user['role'];
}
}

134
core/Auth.php Executable file
View File

@@ -0,0 +1,134 @@
<?php
class Auth {
public static function check() {
if (!isset($_SESSION['user_id'])) {
return false;
}
$timeout = defined('SESSION_TIMEOUT') ? SESSION_TIMEOUT : 28800;
if (isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity'] > $timeout)) {
session_destroy();
return false;
}
$_SESSION['last_activity'] = time();
return true;
}
public static function user() {
if (!self::check()) {
return null;
}
return $_SESSION;
}
public static function id() {
return $_SESSION['user_id'] ?? null;
}
public static function role() {
return $_SESSION['role'] ?? null;
}
public static function isAdmin() {
return self::role() === 'ADMIN';
}
public static function isCapturist() {
return self::role() === 'CAPTURIST' || self::isAdmin();
}
public static function isViewer() {
return self::role() === 'VIEWER';
}
public static function isLector() {
return self::role() === 'LECTOR';
}
public static function getAccessibleHouseIds() {
$db = Database::getInstance();
if (self::isAdmin()) {
$result = $db->fetchAll("SELECT id FROM houses");
return array_column($result, 'id');
} elseif (self::isLector()) {
$userId = self::id();
$result = $db->fetchAll(
"SELECT house_id FROM user_house_permissions WHERE user_id = ?",
[$userId]
);
return array_column($result, 'house_id');
} else {
$result = $db->fetchAll("SELECT id FROM houses");
return array_column($result, 'id');
}
}
public static function requireAuth() {
if (!self::check()) {
header('Location: /login.php');
exit;
}
}
public static function requireAdmin() {
self::requireAuth();
if (!self::isAdmin()) {
header('Location: /dashboard.php');
exit;
}
}
public static function requireCapturist() {
self::requireAuth();
if (!self::isCapturist()) {
header('Location: /dashboard.php');
exit;
}
}
public static function login($user) {
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role'];
$_SESSION['first_name'] = $user['first_name'];
$_SESSION['last_name'] = $user['last_name'];
$_SESSION['last_activity'] = time();
$db = Database::getInstance();
$db->execute(
"UPDATE users SET last_login = NOW() WHERE id = ?",
[$user['id']]
);
self::logActivity('login', "Usuario {$user['username']} inició sesión");
}
public static function logout() {
self::logActivity('logout', "Usuario {$_SESSION['username']} cerró sesión");
session_destroy();
header('Location: /login.php');
exit;
}
public static function logActivity($action, $details = '') {
if (!self::check()) {
return;
}
$db = Database::getInstance();
$db->execute(
"INSERT INTO activity_logs (user_id, action, details, ip_address) VALUES (?, ?, ?, ?)",
[
self::id(),
$action,
$details,
$_SERVER['REMOTE_ADDR'] ?? null
]
);
}
}

69
core/Database.php Executable file
View File

@@ -0,0 +1,69 @@
<?php
class Database {
private static $instance = null;
private $pdo;
private function __construct() {
try {
$dsn = "mysql:host=" . DB_HOST . ";port=" . DB_PORT . ";dbname=" . DB_NAME . ";charset=utf8mb4";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
];
$this->pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
} catch (PDOException $e) {
die("Error de conexión a base de datos: " . $e->getMessage());
}
}
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function getConnection() {
return $this->pdo;
}
public function query($sql, $params = []) {
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt;
}
public function fetchAll($sql, $params = []) {
$stmt = $this->query($sql, $params);
return $stmt->fetchAll();
}
public function fetchOne($sql, $params = []) {
$stmt = $this->query($sql, $params);
return $stmt->fetch();
}
public function execute($sql, $params = []) {
$stmt = $this->query($sql, $params);
return $stmt->rowCount();
}
public function lastInsertId() {
return $this->pdo->lastInsertId();
}
public function beginTransaction() {
return $this->pdo->beginTransaction();
}
public function commit() {
return $this->pdo->commit();
}
public function rollback() {
return $this->pdo->rollBack();
}
}

116
core/JWT.php Executable file
View File

@@ -0,0 +1,116 @@
<?php
class JWT {
private static $secret = '';
private static $algorithm = 'HS256';
private static $tokenExpiration = 86400; // 24 horas en segundos
private static function getSecret() {
if (empty(self::$secret)) {
if (defined('JWT_SECRET')) {
self::$secret = JWT_SECRET;
} else {
self::$secret = 'ibiza_secret_key_change_in_production_2025';
}
}
return self::$secret;
}
private static function getExpiration() {
if (defined('JWT_EXPIRATION')) {
return JWT_EXPIRATION;
}
return self::$tokenExpiration;
}
public static function encode($payload) {
$header = json_encode([
'typ' => 'JWT',
'alg' => self::$algorithm
]);
$payload = array_merge($payload, [
'iat' => time(),
'exp' => time() + self::getExpiration()
]);
$base64UrlHeader = self::base64UrlEncode($header);
$base64UrlPayload = self::base64UrlEncode(json_encode($payload));
$signature = hash_hmac(
'sha256',
$base64UrlHeader . "." . $base64UrlPayload,
self::getSecret(),
true
);
$base64UrlSignature = self::base64UrlEncode($signature);
return $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature;
}
public static function decode($token) {
$tokenParts = explode('.', $token);
if (count($tokenParts) !== 3) {
return null;
}
list($header, $payload, $signature) = $tokenParts;
$validSignature = hash_hmac(
'sha256',
$header . "." . $payload,
self::getSecret(),
true
);
if (!hash_equals(self::base64UrlEncode($validSignature), $signature)) {
return null;
}
$decoded = json_decode(self::base64UrlDecode($payload), true);
if (isset($decoded['exp']) && $decoded['exp'] < time()) {
return null;
}
return $decoded;
}
public static function getUserId($token) {
$decoded = self::decode($token);
return $decoded['user_id'] ?? null;
}
public static function getUserData($token) {
return self::decode($token);
}
public static function refreshToken($token) {
$decoded = self::decode($token);
if (!$decoded) {
return null;
}
// Verificar si está en el período de gracia (última hora)
if ($decoded['exp'] - time() < 3600) {
return self::encode([
'user_id' => $decoded['user_id'],
'username' => $decoded['username'],
'role' => $decoded['role']
]);
}
return null;
}
private static function base64UrlEncode($data) {
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
private static function base64UrlDecode($data) {
return base64_decode(strtr($data, '-_', '+/'));
}
}

81
core/Paginator.php Executable file
View File

@@ -0,0 +1,81 @@
<?php
class Paginator {
private $page;
private $perPage;
private $perPageOptions;
private $total;
public function __construct($page = 1, $perPage = 20, $perPageOptions = [10, 20, 50, 100]) {
$this->page = max(1, (int)$page);
$this->perPage = in_array($perPage, $perPageOptions) ? (int)$perPage : 20;
$this->perPageOptions = $perPageOptions;
}
public static function fromRequest() {
$page = $_GET['page'] ?? $_POST['page'] ?? 1;
$perPage = $_GET['per_page'] ?? $_POST['per_page'] ?? 20;
return new self($page, $perPage);
}
public function getOffset() {
return ($this->page - 1) * $this->perPage;
}
public function getLimit() {
return $this->perPage;
}
public function getPage() {
return $this->page;
}
public function getPerPage() {
return $this->perPage;
}
public function setTotal($total) {
$this->total = $total;
}
public function getTotal() {
return $this->total;
}
public function getTotalPages() {
return (int)ceil($this->total / $this->perPage);
}
public function hasNextPage() {
return $this->page < $this->getTotalPages();
}
public function hasPrevPage() {
return $this->page > 1;
}
public function getNextPage() {
return $this->page + 1;
}
public function getPrevPage() {
return $this->page - 1;
}
public function toArray() {
return [
'page' => $this->page,
'per_page' => $this->perPage,
'total' => $this->total,
'total_pages' => $this->getTotalPages(),
'has_next' => $this->hasNextPage(),
'has_prev' => $this->hasPrevPage(),
'next_page' => $this->hasNextPage() ? $this->getNextPage() : null,
'prev_page' => $this->hasPrevPage() ? $this->getPrevPage() : null
];
}
public function getLimitSql() {
return "LIMIT {$this->perPage} OFFSET {$this->getOffset()}";
}
}

123
core/RateLimiter.php Executable file
View File

@@ -0,0 +1,123 @@
<?php
class RateLimiter {
private $maxRequestsPerMinute = 60;
private $maxRequestsPerHour = 1000;
public function __construct($maxPerMinute = 60, $maxPerHour = 1000) {
$this->maxRequestsPerMinute = $maxPerMinute;
$this->maxRequestsPerHour = $maxPerHour;
}
public function checkRateLimit($identifier) {
$now = time();
$oneMinuteAgo = $now - 60;
$oneHourAgo = $now - 3600;
$minuteKey = "rate_limit_minute:$identifier";
$hourKey = "rate_limit_hour:$identifier";
// Obtener datos actuales
$minuteData = $this->getRateData($minuteKey);
$hourData = $this->getRateData($hourKey);
// Limpiar peticiones viejas
$minuteRequests = array_filter($minuteData, function($timestamp) use ($oneMinuteAgo) {
return $timestamp >= $oneMinuteAgo;
});
$hourRequests = array_filter($hourData, function($timestamp) use ($oneHourAgo) {
return $timestamp >= $oneHourAgo;
});
// Calcular peticiones restantes
$remainingMinute = max(0, $this->maxRequestsPerMinute - count($minuteRequests));
$remainingHour = max(0, $this->maxRequestsPerHour - count($hourRequests));
// Si excede límite por minuto
if (count($minuteRequests) >= $this->maxRequestsPerMinute) {
$retryAfter = min($minuteRequests) + 60;
$this->setRateLimitHeaders($remainingMinute, $this->maxRequestsPerMinute, $retryAfter - $now);
throw new RateLimitException('Demasiadas peticiones. Máximo ' . $this->maxRequestsPerMinute . ' por minuto.', $retryAfter - $now);
}
// Si excede límite por hora
if (count($hourRequests) >= $this->maxRequestsPerHour) {
$retryAfter = min($hourRequests) + 3600;
$this->setRateLimitHeaders($remainingHour, $this->maxRequestsPerHour, $retryAfter - $now);
throw new RateLimitException('Demasiadas peticiones. Máximo ' . $this->maxRequestsPerHour . ' por hora.', $retryAfter - $now);
}
// Agregar petición actual
$minuteRequests[] = $now;
$hourRequests[] = $now;
// Guardar datos actualizados
$this->setRateData($minuteKey, $minuteRequests, 120);
$this->setRateData($hourKey, $hourRequests, 3600);
// Establecer headers de rate limit
$this->setRateLimitHeaders($remainingMinute, $this->maxRequestsPerMinute, null);
return true;
}
private function getRateData($key) {
// Usar sesión para rate limiting
if (session_status() === PHP_SESSION_ACTIVE && isset($_SESSION[$key])) {
return $_SESSION[$key];
}
return [];
}
private function setRateData($key, $data, $ttl = null) {
// Usar sesión para rate limiting
if (session_status() === PHP_SESSION_ACTIVE) {
$_SESSION[$key] = $data;
}
}
private function setRateLimitHeaders($remaining, $limit, $retryAfter = null) {
header("X-RateLimit-Limit: $limit");
header("X-RateLimit-Remaining: $remaining");
if ($retryAfter !== null) {
header("X-RateLimit-Reset: $retryAfter");
header("Retry-After: $retryAfter");
}
}
public function getIdentifierFromRequest() {
// Usar IP como identificador por defecto
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['HTTP_CLIENT_IP'] ?? $_SERVER['REMOTE_ADDR'] ?? 'unknown';
// Si hay token JWT, usar user_id + IP para limitar por usuario
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? '';
if (preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
require_once __DIR__ . '/JWT.php';
$token = $matches[1];
$decoded = JWT::decode($token);
if ($decoded && isset($decoded['user_id'])) {
return 'user_' . $decoded['user_id'];
}
}
return 'ip_' . $ip;
}
}
class RateLimitException extends Exception {
private $retryAfter;
public function __construct($message, $retryAfter) {
parent::__construct($message);
$this->retryAfter = $retryAfter;
}
public function getRetryAfter() {
return $this->retryAfter;
}
}