Initial commit: Uruk Prize Platform architecture, backend core, mobile app, gateway caller & deployment pipeline

This commit is contained in:
Hamza-Ayed
2026-09-18 17:12:05 +03:00
commit 879dd36f1b
1149 changed files with 97122 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Core;
use PDO;
use PDOException;
use RuntimeException;
class Database
{
private static ?PDO $instance = null;
private function __construct() {}
private function __clone() {}
public static function getConnection(): PDO
{
if (self::$instance === null) {
$config = require __DIR__ . '/../config/database.php';
$dsn = sprintf(
'mysql:host=%s;port=%d;dbname=%s;charset=%s',
$config['host'],
$config['port'],
$config['database'],
$config['charset']
);
try {
self::$instance = new PDO(
$dsn,
$config['username'],
$config['password'],
$config['options']
);
} catch (PDOException $e) {
throw new RuntimeException('Database Connection Failed: ' . $e->getMessage(), (int)$e->getCode());
}
}
return self::$instance;
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Core;
abstract class Middleware
{
abstract public function handle(Request $request, callable $next): void;
}
+85
View File
@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Core;
use Redis;
use Throwable;
class RedisClient
{
private static ?Redis $instance = null;
private static bool $connectionFailed = false;
private function __construct() {}
private function __clone() {}
public static function getInstance(): ?Redis
{
if (self::$connectionFailed) {
return null;
}
if (self::$instance === null) {
if (!class_exists('Redis')) {
self::$connectionFailed = true;
return null;
}
try {
$config = require __DIR__ . '/../config/redis.php';
$redis = new Redis();
$connected = $redis->connect($config['host'], $config['port'], (float)$config['timeout']);
if ($connected) {
if (!empty($config['password'])) {
$redis->auth($config['password']);
}
if (!empty($config['database'])) {
$redis->select((int)$config['database']);
}
$redis->setOption(Redis::OPT_PREFIX, $config['prefix']);
self::$instance = $redis;
} else {
self::$connectionFailed = true;
}
} catch (Throwable $e) {
self::$connectionFailed = true;
error_log('[Redis Error] ' . $e->getMessage());
return null;
}
}
return self::$instance;
}
public static function get(string $key): ?string
{
$redis = self::getInstance();
if (!$redis) return null;
$val = $redis->get($key);
return $val !== false ? (string)$val : null;
}
public static function set(string $key, string $value, int $ttlSeconds = 0): bool
{
$redis = self::getInstance();
if (!$redis) return false;
return $ttlSeconds > 0 ? (bool)$redis->setex($key, $ttlSeconds, $value) : (bool)$redis->set($key, $value);
}
public static function del(string $key): bool
{
$redis = self::getInstance();
if (!$redis) return false;
return (bool)$redis->del($key);
}
public static function has(string $key): bool
{
$redis = self::getInstance();
if (!$redis) return false;
return (bool)$redis->exists($key);
}
}
+112
View File
@@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace Core;
class Request
{
private string $method;
private string $path;
private array $headers;
private array $queryParams;
private array $body;
private string $rawBody;
public function __construct()
{
$this->method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
$uri = $_SERVER['REQUEST_URI'] ?? '/';
$this->path = parse_url($uri, PHP_URL_PATH) ?: '/';
$this->queryParams = $_GET ?? [];
$this->headers = $this->extractHeaders();
$this->rawBody = file_get_contents('php://input') ?: '';
$contentType = $this->getHeader('content-type');
if (str_contains($contentType, 'application/json') && !empty($this->rawBody)) {
$decoded = json_decode($this->rawBody, true);
$this->body = is_array($decoded) ? $decoded : [];
} else {
$this->body = $_POST ?? [];
}
}
private function extractHeaders(): array
{
$headers = [];
foreach ($_SERVER as $key => $value) {
if (str_starts_with($key, 'HTTP_')) {
$headerName = strtolower(str_replace('_', '-', substr($key, 5)));
$headers[$headerName] = (string)$value;
} elseif (in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH'], true)) {
$headerName = strtolower(str_replace('_', '-', $key));
$headers[$headerName] = (string)$value;
}
}
return $headers;
}
public function getMethod(): string
{
return $this->method;
}
public function getPath(): string
{
return $this->path;
}
public function getHeader(string $name, string $default = ''): string
{
$name = strtolower($name);
return $this->headers[$name] ?? $default;
}
public function getHeaders(): array
{
return $this->headers;
}
public function getQueryParams(): array
{
return $this->queryParams;
}
public function getQuery(string $key, $default = null)
{
return $this->queryParams[$key] ?? $default;
}
public function getBody(): array
{
return $this->body;
}
public function get(string $key, $default = null)
{
return $this->body[$key] ?? $default;
}
public function getRawBody(): string
{
return $this->rawBody;
}
public function getIp(): string
{
if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])) {
return $_SERVER['HTTP_CF_CONNECTING_IP'];
}
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
return trim($ips[0]);
}
return $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
}
public function getUserAgent(): string
{
return $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown';
}
}
+67
View File
@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Core;
class Response
{
public static function json(array $data, int $statusCode = 200, array $headers = []): void
{
http_response_code($statusCode);
$defaultHeaders = [
'Content-Type' => 'application/json; charset=utf-8',
'X-Content-Type-Options' => 'nosniff',
'X-Frame-Options' => 'DENY',
'X-XSS-Protection' => '1; mode=block',
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, X-User-Id, X-Device-Fingerprint, X-Timestamp, X-Nonce, X-Signature',
];
foreach (array_merge($defaultHeaders, $headers) as $name => $val) {
header("$name: $val");
}
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
public static function success(mixed $data = null, string $message = 'Success', int $statusCode = 200): void
{
self::json([
'success' => true,
'status' => 'success',
'message' => $message,
'data' => $data,
'timestamp' => time(),
], $statusCode);
}
public static function error(string $message = 'An error occurred', int $statusCode = 400, array $errors = []): void
{
self::json([
'success' => false,
'status' => 'error',
'message' => $message,
'errors' => $errors,
'timestamp' => time(),
], $statusCode);
}
public static function unauthorized(string $message = 'Unauthorized access'): void
{
self::error($message, 401);
}
public static function forbidden(string $message = 'Forbidden'): void
{
self::error($message, 403);
}
public static function notFound(string $message = 'Resource not found'): void
{
self::error($message, 404);
}
}
+119
View File
@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace Core;
class Router
{
private array $routes = [];
public function get(string $path, array|callable $handler, array $middlewares = []): void
{
$this->addRoute('GET', $path, $handler, $middlewares);
}
public function post(string $path, array|callable $handler, array $middlewares = []): void
{
$this->addRoute('POST', $path, $handler, $middlewares);
}
public function put(string $path, array|callable $handler, array $middlewares = []): void
{
$this->addRoute('PUT', $path, $handler, $middlewares);
}
public function delete(string $path, array|callable $handler, array $middlewares = []): void
{
$this->addRoute('DELETE', $path, $handler, $middlewares);
}
public function options(string $path, array|callable $handler): void
{
$this->addRoute('OPTIONS', $path, $handler, []);
}
private function addRoute(string $method, string $path, array|callable $handler, array $middlewares): void
{
$pattern = preg_replace('/\{([a-zA-Z0-9_]+)\}/', '(?P<$1>[^/]+)', $path);
$pattern = '#^' . $pattern . '$#';
$this->routes[] = [
'method' => $method,
'path' => $path,
'pattern' => $pattern,
'handler' => $handler,
'middlewares' => $middlewares,
];
}
public function dispatch(Request $request): void
{
// Handle preflight CORS OPTIONS requests immediately
if ($request->getMethod() === 'OPTIONS') {
Response::json(['status' => 'ok']);
}
$method = $request->getMethod();
$path = $request->getPath();
foreach ($this->routes as $route) {
if ($route['method'] !== $method) {
continue;
}
if (preg_match($route['pattern'], $path, $matches)) {
$params = [];
foreach ($matches as $key => $value) {
if (is_string($key)) {
$params[$key] = $value;
}
}
$this->runMiddlewares($route['middlewares'], $request, function () use ($route, $request, $params) {
$this->executeHandler($route['handler'], $request, $params);
});
return;
}
}
Response::notFound("Endpoint not found: [{$method}] {$path}");
}
private function runMiddlewares(array $middlewares, Request $request, callable $target): void
{
$pipeline = array_reduce(
array_reverse($middlewares),
function ($next, $middlewareClass) {
return function ($req) use ($next, $middlewareClass) {
$instance = new $middlewareClass();
$instance->handle($req, $next);
};
},
$target
);
$pipeline($request);
}
private function executeHandler(array|callable $handler, Request $request, array $params): void
{
if (is_callable($handler)) {
call_user_func($handler, $request, $params);
return;
}
[$controllerClass, $methodName] = $handler;
if (!class_exists($controllerClass)) {
Response::error("Controller class {$controllerClass} not found", 500);
}
$controller = new $controllerClass();
if (!method_exists($controller, $methodName)) {
Response::error("Method {$methodName} not found on controller {$controllerClass}", 500);
}
$controller->$methodName($request, $params);
}
}
+124
View File
@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace Core;
class Security
{
public static function hashPassword(string $password): string
{
$algo = defined('PASSWORD_ARGON2ID') ? PASSWORD_ARGON2ID : PASSWORD_BCRYPT;
return password_hash($password, $algo);
}
public static function verifyPassword(string $password, string $hash): bool
{
return password_verify($password, $hash);
}
public static function generateRandomHex(int $bytes = 32): string
{
return bin2hex(random_bytes($bytes));
}
public static function generateHmacSignature(string $data, string $secretKey): string
{
return hash_hmac('sha256', $data, $secretKey);
}
public static function verifyHmacSignature(string $data, string $signature, string $secretKey): bool
{
$calculated = self::generateHmacSignature($data, $secretKey);
return hash_equals($calculated, $signature);
}
public static function encryptAesGcm(string $plaintext, string $key): string
{
$cipher = 'aes-256-gcm';
$keyHash = hash('sha256', $key, true);
$iv = random_bytes(12); // 96-bit IV recommended for GCM
$tag = '';
$ciphertext = openssl_encrypt(
$plaintext,
$cipher,
$keyHash,
OPENSSL_RAW_DATA,
$iv,
$tag,
'',
16
);
return base64_encode($iv . $tag . $ciphertext);
}
public static function decryptAesGcm(string $encryptedPackage, string $key): ?string
{
$data = base64_decode($encryptedPackage, true);
if (!$data || strlen($data) < 28) {
return null;
}
$cipher = 'aes-256-gcm';
$keyHash = hash('sha256', $key, true);
$iv = substr($data, 0, 12);
$tag = substr($data, 12, 16);
$ciphertext = substr($data, 28);
$decrypted = openssl_decrypt(
$ciphertext,
$cipher,
$keyHash,
OPENSSL_RAW_DATA,
$iv,
$tag
);
return $decrypted !== false ? $decrypted : null;
}
public static function generateDynamicQrToken(int $userId, string $membershipNumber, int $expiresInSeconds, string $secretKey): string
{
$payload = [
'uid' => $userId,
'mem' => $membershipNumber,
'exp' => time() + $expiresInSeconds,
'rnd' => self::generateRandomHex(8),
];
$json = json_encode($payload, JSON_UNESCAPED_SLASHES);
$base64Payload = rtrim(strtr(base64_encode($json), '+/', '-_'), '=');
$signature = self::generateHmacSignature($base64Payload, $secretKey);
return $base64Payload . '.' . $signature;
}
public static function verifyDynamicQrToken(string $token, string $secretKey): ?array
{
$parts = explode('.', $token);
if (count($parts) !== 2) {
return null;
}
[$base64Payload, $signature] = $parts;
if (!self::verifyHmacSignature($base64Payload, $signature, $secretKey)) {
return null;
}
$json = base64_decode(strtr($base64Payload, '-_', '+/'));
if (!$json) {
return null;
}
$payload = json_decode($json, true);
if (!is_array($payload) || !isset($payload['exp']) || $payload['exp'] < time()) {
return null; // Expired or invalid format
}
return $payload;
}
}