Primer version funcional
This commit is contained in:
133
core/ApiAuth.php
Executable file
133
core/ApiAuth.php
Executable 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
134
core/Auth.php
Executable 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
69
core/Database.php
Executable 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
116
core/JWT.php
Executable 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
81
core/Paginator.php
Executable 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
123
core/RateLimiter.php
Executable 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user