first commit

This commit is contained in:
Hamza-Ayed
2026-05-23 16:17:20 +03:00
commit 2bbaa1ee16
195 changed files with 11126 additions and 0 deletions

96
backend/includes/Auth.php Normal file
View File

@@ -0,0 +1,96 @@
<?php
/**
* Authentication — App Key Validation
*/
require_once __DIR__ . '/../config.php';
class Auth
{
/**
* Validate the app_key from request.
* Supports both Flutter app key and Caller device key.
*
* @param string|null $key The key provided in request
* @param string $required Which key type is required: 'app' | 'device' | 'any'
* @return bool
*/
public static function validate(?string $key, string $required = 'any'): bool
{
if ($key === null || $key === '') {
return false;
}
switch ($required) {
case 'app':
return hash_equals(APP_KEY, $key);
case 'device':
return hash_equals(DEVICE_KEY, $key);
case 'any':
return hash_equals(APP_KEY, $key) || hash_equals(DEVICE_KEY, $key);
default:
return false;
}
}
/**
* Extract app_key from request (header or body).
*/
public static function getKeyFromRequest(): ?string
{
// Check header first
$headerKey = $_SERVER['HTTP_X_APP_KEY']
?? $_SERVER['HTTP_APP_KEY']
?? null;
if ($headerKey !== null) {
return $headerKey;
}
// Check JSON body
$body = json_decode(file_get_contents('php://input'), true);
if (is_array($body) && isset($body['app_key'])) {
return $body['app_key'];
}
// Check POST data
if (isset($_POST['app_key'])) {
return $_POST['app_key'];
}
return null;
}
/**
* Require authentication — sends 401 and exits on failure.
*/
public static function requireAuth(string $required = 'any'): void
{
$key = self::getKeyFromRequest();
if (!self::validate($key, $required)) {
http_response_code(401);
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'invalid_app_key',
]);
exit;
}
}
/**
* Determine if the provided key is the device key.
*/
public static function isDeviceKey(?string $key): bool
{
return $key !== null && hash_equals(DEVICE_KEY, $key);
}
/**
* Determine if the provided key is the app key.
*/
public static function isAppKey(?string $key): bool
{
return $key !== null && hash_equals(APP_KEY, $key);
}
}

View File

@@ -0,0 +1,49 @@
<?php
/**
* Database Singleton — PDO Connection
*/
require_once __DIR__ . '/../config.php';
class Database
{
private static ?PDO $instance = null;
private function __construct()
{
try {
$dsn = sprintf(
'mysql:host=%s;dbname=%s;charset=utf8mb4',
DB_HOST,
DB_NAME
);
self::$instance = new PDO($dsn, DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::MYSQL_ATTR_INIT_COMMAND => "SET time_zone = '+03:00'",
]);
} catch (PDOException $e) {
error_log('Database connection failed: ' . $e->getMessage());
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'database_error']);
exit;
}
}
public static function getInstance(): PDO
{
if (self::$instance === null) {
new self();
}
return self::$instance;
}
/** Prevent cloning */
private function __clone() {}
public function __wakeup()
{
throw new \Exception("Cannot unserialize singleton");
}
}

View File

@@ -0,0 +1,61 @@
<?php
/**
* Request Logger — Logs all API requests to MySQL
*/
require_once __DIR__ . '/Database.php';
class RequestLogger
{
/**
* Log an API request.
*/
public static function log(
string $endpoint,
string $method,
?array $requestBody = null,
int $responseCode = 200,
?string $error = null
): void {
if (!LOG_REQUESTS) {
return;
}
try {
$db = Database::getInstance();
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
$body = $requestBody ? json_encode($requestBody) : null;
// Mask sensitive fields
if ($body) {
$body = self::maskSensitive($body);
}
$stmt = $db->prepare(
"INSERT INTO api_logs (endpoint, method, ip_address, user_agent, request_body, response_code, error, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, NOW())"
);
$stmt->execute([$endpoint, $method, $ip, $userAgent, $body, $responseCode, $error]);
} catch (\Throwable $e) {
// Logging should never break the app
error_log("RequestLogger error: " . $e->getMessage());
}
}
/**
* Mask sensitive fields in request body.
*/
private static function maskSensitive(string $body): string
{
$sensitive = ['app_key', 'password', 'otp', 'otp_code'];
foreach ($sensitive as $field) {
$body = preg_replace(
'/"' . $field . '"\s*:\s*"[^"]*"/',
'"' . $field . '":"***"',
$body
);
}
return $body;
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* Rate Limiting via Redis
*/
require_once __DIR__ . '/Redis.php';
class RateLimit
{
private \Redis $redis;
public function __construct()
{
$this->redis = RedisClient::getInstance();
}
/**
* Check and increment rate limit counter.
*
* @param string $key Identifier (e.g. "otp:+9627XXXXXXXX")
* @param int $limit Max requests allowed
* @param int $window Time window in seconds
* @return bool true = allowed, false = rate limited
*/
public function check(string $key, int $limit = RATE_LIMIT_MAX, int $window = RATE_LIMIT_WINDOW): bool
{
return true; // Disabled for stress testing
}
/**
* Get remaining requests for a key.
*/
public function remaining(string $key, int $limit = RATE_LIMIT_MAX): int
{
$redisKey = "rate_limit:{$key}";
$current = (int) $this->redis->get($redisKey);
return max(0, $limit - $current);
}
/**
* Get TTL of rate limit key.
*/
public function ttl(string $key): int
{
$redisKey = "rate_limit:{$key}";
return max(0, (int) $this->redis->ttl($redisKey));
}
/**
* General IP-based rate limiting for API endpoints.
*
* @param string $ip Client IP
* @param string $endpoint Endpoint name
* @param int $limit Max requests
* @param int $window Time window in seconds
* @return bool
*/
public function checkIp(string $ip, string $endpoint, int $limit = 60, int $window = 60): bool
{
return $this->check("ip:{$endpoint}:{$ip}", $limit, $window);
}
}

View File

@@ -0,0 +1,55 @@
<?php
/**
* Redis Singleton — phpredis Extension
*/
require_once __DIR__ . '/../config.php';
class RedisClient
{
private static ?\Redis $instance = null;
private function __construct()
{
try {
self::$instance = new \Redis();
self::$instance->connect(REDIS_HOST, REDIS_PORT, 2.0);
if (REDIS_PASSWORD !== null) {
self::$instance->auth(REDIS_PASSWORD);
}
if (REDIS_DB > 0) {
self::$instance->select(REDIS_DB);
}
self::$instance->setOption(\Redis::OPT_SERIALIZER, \Redis::SERIALIZER_JSON);
} catch (\RedisException $e) {
$errorMsg = $e->getMessage();
error_log('Redis connection failed: ' . $errorMsg);
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'cache_error', 'error' => $errorMsg]);
exit;
} catch (\Exception $e) {
$errorMsg = $e->getMessage();
error_log('Unexpected error in Redis: ' . $errorMsg);
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'cache_error', 'error' => $errorMsg]);
exit;
}
}
public static function getInstance(): \Redis
{
if (self::$instance === null) {
new self();
}
return self::$instance;
}
private function __clone() {}
public function __wakeup()
{
throw new \Exception("Cannot unserialize singleton");
}
}