refactor: odstraneni PSR-1 SideEffects warningu
- Handler funkce extrahovany z API souboru do api/admin/handlers/ - config.php rozdeleny na helpers.php (funkce) a constants.php (konstanty) - require_once odstranen z class souboru (AuditLog, JWTAuth, LeaveNotification) - vendor/autoload.php presunuto do config.php bootstrap - totp-handlers.php: pridany use deklarace pro TwoFactorAuth - phpstan.neon: bootstrapFiles, scanDirectories, dynamicConstantNames - Opraveny chybejici routing bloky v totp.php a session.php Vysledek: phpcs 0 errors 0 warnings, PHPStan 0 errors, ESLint 0 errors Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
392
api/config.php
392
api/config.php
@@ -1,60 +1,20 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* BOHA Automation - API Configuration
|
||||
* BOHA Automation - API Configuration Bootstrap
|
||||
*
|
||||
* Database and application configuration
|
||||
* Nacte helper funkce, env promenne a konstanty.
|
||||
* Toto je jediny soubor, ktery API endpointy musi require_once.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Load .env file
|
||||
function loadEnv(string $path): bool
|
||||
{
|
||||
if (!file_exists($path)) {
|
||||
return false;
|
||||
}
|
||||
require_once __DIR__ . '/includes/helpers.php';
|
||||
require_once dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
if (strpos(trim($line), '#') === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parts = explode('=', $line, 2);
|
||||
if (count($parts) !== 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = trim($parts[0]);
|
||||
$value = trim($parts[1]);
|
||||
|
||||
// Remove quotes if present
|
||||
if (
|
||||
(substr($value, 0, 1) === '"' && substr($value, -1) === '"') ||
|
||||
(substr($value, 0, 1) === "'" && substr($value, -1) === "'")
|
||||
) {
|
||||
$value = substr($value, 1, -1);
|
||||
}
|
||||
|
||||
$_ENV[$name] = $value;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Load .env from api directory
|
||||
loadEnv(__DIR__ . '/.env');
|
||||
|
||||
// Helper function to get env value with default
|
||||
function env(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $_ENV[$key] ?? $default;
|
||||
}
|
||||
|
||||
// Environment
|
||||
define('APP_ENV', env('APP_ENV', 'production'));
|
||||
define('DEBUG_MODE', APP_ENV === 'local');
|
||||
require_once __DIR__ . '/includes/constants.php';
|
||||
|
||||
if (DEBUG_MODE) {
|
||||
error_reporting(E_ALL);
|
||||
@@ -63,343 +23,3 @@ if (DEBUG_MODE) {
|
||||
error_reporting(0);
|
||||
ini_set('display_errors', 0);
|
||||
}
|
||||
|
||||
// Database configuration
|
||||
define('DB_HOST', env('DB_HOST', 'localhost'));
|
||||
define('DB_NAME', env('DB_NAME', ''));
|
||||
define('DB_USER', env('DB_USER', ''));
|
||||
define('DB_PASS', env('DB_PASS', ''));
|
||||
define('DB_CHARSET', 'utf8mb4');
|
||||
|
||||
// Security configuration
|
||||
define('MAX_LOGIN_ATTEMPTS', 5);
|
||||
define('LOCKOUT_MINUTES', 15);
|
||||
define('BCRYPT_COST', 12);
|
||||
|
||||
// CORS - aktualizuj po nasazeni na subdomenu
|
||||
define('CORS_ALLOWED_ORIGINS', [
|
||||
'http://www.boha-automation.cz',
|
||||
'https://www.boha-automation.cz',
|
||||
]);
|
||||
|
||||
// Paths
|
||||
define('API_ROOT', __DIR__);
|
||||
define('INCLUDES_PATH', API_ROOT . '/includes');
|
||||
|
||||
// Rate limiting
|
||||
define('RATE_LIMIT_STORAGE_PATH', __DIR__ . '/rate_limits');
|
||||
|
||||
|
||||
/**
|
||||
* Get PDO database connection (singleton pattern)
|
||||
*
|
||||
* @return PDO Database connection instance
|
||||
* @throws PDOException If connection fails
|
||||
*/
|
||||
function db(): PDO
|
||||
{
|
||||
static $pdo = null;
|
||||
|
||||
if ($pdo === null) {
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;dbname=%s;charset=%s',
|
||||
DB_HOST,
|
||||
DB_NAME,
|
||||
DB_CHARSET
|
||||
);
|
||||
|
||||
$options = [
|
||||
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 NAMES ' . DB_CHARSET,
|
||||
];
|
||||
|
||||
try {
|
||||
$pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
|
||||
} catch (PDOException $e) {
|
||||
if (DEBUG_MODE) {
|
||||
throw $e;
|
||||
}
|
||||
error_log('Database connection failed: ' . $e->getMessage());
|
||||
throw new PDOException('Database connection failed');
|
||||
}
|
||||
}
|
||||
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set CORS headers for API responses
|
||||
*/
|
||||
function setCorsHeaders(): void
|
||||
{
|
||||
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
||||
|
||||
if (in_array($origin, CORS_ALLOWED_ORIGINS)) {
|
||||
header("Access-Control-Allow-Origin: $origin");
|
||||
header('Access-Control-Allow-Credentials: true');
|
||||
} elseif (DEBUG_MODE && str_starts_with($origin, 'http://127.0.0.1:')) {
|
||||
header("Access-Control-Allow-Origin: $origin");
|
||||
header('Access-Control-Allow-Credentials: true');
|
||||
}
|
||||
// Neznamy origin = zadny CORS header
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');
|
||||
header('Access-Control-Max-Age: 86400');
|
||||
|
||||
// Handle preflight requests
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(200);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send JSON response and exit
|
||||
*
|
||||
* @param mixed $data Data to send
|
||||
* @param int $statusCode HTTP status code
|
||||
*/
|
||||
function jsonResponse($data, int $statusCode = 200): void
|
||||
{
|
||||
http_response_code($statusCode);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($data, JSON_UNESCAPED_UNICODE);
|
||||
exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send error response
|
||||
*
|
||||
* @param string $message Error message
|
||||
* @param int $statusCode HTTP status code
|
||||
*/
|
||||
function errorResponse(string $message, int $statusCode = 400): void
|
||||
{
|
||||
jsonResponse(['success' => false, 'error' => $message], $statusCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send success response
|
||||
*
|
||||
* @param mixed $data Data to include
|
||||
* @param string $message Optional message
|
||||
*/
|
||||
function successResponse($data = null, string $message = ''): void
|
||||
{
|
||||
$response = ['success' => true];
|
||||
if ($message) {
|
||||
$response['message'] = $message;
|
||||
}
|
||||
if ($data !== null) {
|
||||
$response['data'] = $data;
|
||||
}
|
||||
jsonResponse($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JSON request body
|
||||
*
|
||||
* @return array<string, mixed> Decoded JSON data
|
||||
*/
|
||||
function getJsonInput(): array
|
||||
{
|
||||
$input = file_get_contents('php://input');
|
||||
$data = json_decode($input, true);
|
||||
return is_array($data) ? $data : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize string input
|
||||
*
|
||||
* @param string $input Input string
|
||||
* @return string Sanitized string
|
||||
*/
|
||||
function sanitize(string $input): string
|
||||
{
|
||||
return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate email format
|
||||
*
|
||||
* @param string $email Email to validate
|
||||
* @return bool True if valid
|
||||
*/
|
||||
function isValidEmail(string $email): bool
|
||||
{
|
||||
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and sanitize month parameter (YYYY-MM format)
|
||||
*/
|
||||
function validateMonth(string $param = 'month'): string
|
||||
{
|
||||
$month = $_GET[$param] ?? date('Y-m');
|
||||
if (!preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $month)) {
|
||||
$month = date('Y-m');
|
||||
}
|
||||
return $month;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get client IP address
|
||||
*
|
||||
* Uses only REMOTE_ADDR which cannot be spoofed (TCP connection IP).
|
||||
* If you add a reverse proxy (Cloudflare, Nginx, etc.) in the future,
|
||||
* update this function to trust specific proxy headers only from known proxy IPs.
|
||||
*
|
||||
* @return string IP address
|
||||
*/
|
||||
function getClientIp(): string
|
||||
{
|
||||
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set security headers for API responses
|
||||
*
|
||||
* Sets standard security headers to protect against common web vulnerabilities:
|
||||
* - X-Content-Type-Options: Prevents MIME type sniffing
|
||||
* - X-Frame-Options: Prevents clickjacking attacks
|
||||
* - X-XSS-Protection: Enables browser XSS filter
|
||||
* - Referrer-Policy: Controls referrer information sent with requests
|
||||
*
|
||||
* Note: Content-Security-Policy is not set here as it may interfere with the React frontend
|
||||
*/
|
||||
function setSecurityHeaders(): void
|
||||
{
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('X-Frame-Options: DENY');
|
||||
header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||
if (!DEBUG_MODE && isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
|
||||
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set no-cache headers
|
||||
*
|
||||
* Prevents browser caching for sensitive endpoints
|
||||
*/
|
||||
function setNoCacheHeaders(): void
|
||||
{
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||||
header('Cache-Control: post-check=0, pre-check=0', false);
|
||||
header('Pragma: no-cache');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sdilene generovani cisel pro objednavky a projekty (spolecny ciselny prostor)
|
||||
*/
|
||||
function generateSharedNumber(PDO $pdo): string
|
||||
{
|
||||
$yy = date('y');
|
||||
|
||||
$settings = $pdo->query('SELECT order_type_code FROM company_settings LIMIT 1')->fetch();
|
||||
$typeCode = ($settings && !empty($settings['order_type_code'])) ? $settings['order_type_code'] : '71';
|
||||
|
||||
$prefix = $yy . $typeCode;
|
||||
$prefixLen = strlen($prefix);
|
||||
$likePattern = $prefix . '%';
|
||||
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT COALESCE(MAX(seq), 0) FROM (
|
||||
SELECT CAST(SUBSTRING(order_number, ? + 1) AS UNSIGNED) AS seq
|
||||
FROM orders WHERE order_number LIKE ?
|
||||
UNION ALL
|
||||
SELECT CAST(SUBSTRING(project_number, ? + 1) AS UNSIGNED) AS seq
|
||||
FROM projects WHERE project_number LIKE ?
|
||||
) combined
|
||||
');
|
||||
$stmt->execute([$prefixLen, $likePattern, $prefixLen, $likePattern]);
|
||||
$max = (int) $stmt->fetchColumn();
|
||||
|
||||
return sprintf('%s%s%04d', $yy, $typeCode, $max + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get permissions for a user by their ID
|
||||
* Cached per-request via static variable
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
function getUserPermissions(int $userId): array
|
||||
{
|
||||
static $cache = [];
|
||||
|
||||
if (isset($cache[$userId])) {
|
||||
return $cache[$userId];
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
|
||||
// Check if user has admin role (superuser bypass)
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT r.name FROM users u
|
||||
JOIN roles r ON u.role_id = r.id
|
||||
WHERE u.id = ?
|
||||
');
|
||||
$stmt->execute([$userId]);
|
||||
$role = $stmt->fetch();
|
||||
|
||||
if ($role && $role['name'] === 'admin') {
|
||||
// Admin gets all permissions
|
||||
$stmt = $pdo->query('SELECT name FROM permissions');
|
||||
$cache[$userId] = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
return $cache[$userId];
|
||||
}
|
||||
|
||||
// Regular user: get permissions via role_permissions
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT p.name
|
||||
FROM permissions p
|
||||
JOIN role_permissions rp ON p.id = rp.permission_id
|
||||
JOIN users u ON u.role_id = rp.role_id
|
||||
WHERE u.id = ?
|
||||
');
|
||||
$stmt->execute([$userId]);
|
||||
$cache[$userId] = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
return $cache[$userId];
|
||||
} catch (PDOException $e) {
|
||||
error_log('getUserPermissions error: ' . $e->getMessage());
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Require a specific permission, return 403 if denied
|
||||
*
|
||||
* @param array<string, mixed> $authData
|
||||
*/
|
||||
function requirePermission(array $authData, string $permission): void
|
||||
{
|
||||
// Admin superuser bypass
|
||||
if ($authData['user']['is_admin'] ?? false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$permissions = getUserPermissions($authData['user_id']);
|
||||
if (!in_array($permission, $permissions)) {
|
||||
errorResponse('Přístup odepřen. Nemáte potřebná oprávnění.', 403);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has a specific permission (returns bool)
|
||||
*
|
||||
* @param array<string, mixed> $authData
|
||||
*/
|
||||
function hasPermission(array $authData, string $permission): bool
|
||||
{
|
||||
if ($authData['user']['is_admin'] ?? false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$permissions = getUserPermissions($authData['user_id']);
|
||||
return in_array($permission, $permissions);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user