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:
@@ -18,7 +18,7 @@ require_once dirname(__DIR__) . '/includes/JWTAuth.php';
|
||||
require_once dirname(__DIR__) . '/includes/AuditLog.php';
|
||||
require_once dirname(__DIR__) . '/includes/RateLimiter.php';
|
||||
require_once dirname(__DIR__) . '/includes/Encryption.php';
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/handlers/totp-handlers.php';
|
||||
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
use RobThree\Auth\TwoFactorAuthException;
|
||||
@@ -32,16 +32,6 @@ header('Content-Type: application/json; charset=utf-8');
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$action = $_GET['action'] ?? '';
|
||||
|
||||
/** Lazy init - QRServerProvider dela externi HTTP, nepotrebujeme ho pro kazdy request */
|
||||
function getTfa(): TwoFactorAuth
|
||||
{
|
||||
static $tfa = null;
|
||||
if ($tfa === null) {
|
||||
$tfa = new TwoFactorAuth(new QRServerProvider(), 'BOHA Automation');
|
||||
}
|
||||
return $tfa;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
|
||||
@@ -80,409 +70,3 @@ try {
|
||||
error_log('TOTP error: ' . $e->getMessage());
|
||||
errorResponse('Došlo k chybě', 500);
|
||||
}
|
||||
|
||||
/** GET ?action=status */
|
||||
function handleStatus(PDO $pdo): void
|
||||
{
|
||||
$authData = JWTAuth::requireAuth();
|
||||
AuditLog::setUser($authData['user_id'], $authData['user']['username'] ?? 'unknown');
|
||||
$userId = $authData['user_id'];
|
||||
|
||||
$stmt = $pdo->prepare('SELECT totp_enabled FROM users WHERE id = ?');
|
||||
$stmt->execute([$userId]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
successResponse([
|
||||
'totp_enabled' => (bool) ($user['totp_enabled'] ?? false),
|
||||
]);
|
||||
}
|
||||
|
||||
/** POST ?action=setup - vygenerovat secret + QR URI (jeste neaktivuje 2FA) */
|
||||
function handleSetup(PDO $pdo, TwoFactorAuth $tfa): void
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
errorResponse('Metoda není povolena', 405);
|
||||
}
|
||||
|
||||
$authData = JWTAuth::requireAuth();
|
||||
AuditLog::setUser($authData['user_id'], $authData['user']['username'] ?? 'unknown');
|
||||
$userId = $authData['user_id'];
|
||||
|
||||
$stmt = $pdo->prepare('SELECT totp_enabled, username, email FROM users WHERE id = ?');
|
||||
$stmt->execute([$userId]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if ($user['totp_enabled']) {
|
||||
errorResponse('2FA je již aktivní. Nejdříve ji deaktivujte.');
|
||||
}
|
||||
|
||||
$secret = $tfa->createSecret();
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE users SET totp_secret = ? WHERE id = ?');
|
||||
$stmt->execute([Encryption::encrypt($secret), $userId]);
|
||||
|
||||
$label = $user['email'] ?: $user['username'];
|
||||
$qrUri = $tfa->getQRText($label, $secret);
|
||||
|
||||
successResponse([
|
||||
'secret' => $secret,
|
||||
'qr_uri' => $qrUri,
|
||||
]);
|
||||
}
|
||||
|
||||
/** POST ?action=enable { "code": "123456" } */
|
||||
function handleEnable(PDO $pdo, TwoFactorAuth $tfa): void
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
errorResponse('Metoda není povolena', 405);
|
||||
}
|
||||
|
||||
$authData = JWTAuth::requireAuth();
|
||||
AuditLog::setUser($authData['user_id'], $authData['user']['username'] ?? 'unknown');
|
||||
$userId = $authData['user_id'];
|
||||
$input = getJsonInput();
|
||||
$code = trim($input['code'] ?? '');
|
||||
|
||||
if (empty($code)) {
|
||||
errorResponse('Ověřovací kód je povinný');
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare('SELECT totp_secret, totp_enabled FROM users WHERE id = ?');
|
||||
$stmt->execute([$userId]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if (!$user['totp_secret']) {
|
||||
errorResponse('Nejprve vygenerujte tajný klíč (setup)');
|
||||
}
|
||||
|
||||
if ($user['totp_enabled']) {
|
||||
errorResponse('2FA je již aktivní');
|
||||
}
|
||||
|
||||
$decryptedSecret = decryptTotpSecret($user['totp_secret']);
|
||||
if (!$tfa->verifyCode($decryptedSecret, $code)) {
|
||||
errorResponse('Neplatný ověřovací kód. Zkontrolujte čas na telefonu.');
|
||||
}
|
||||
|
||||
$backupCodes = generateBackupCodes();
|
||||
$hashedCodes = array_map(fn ($c) => password_hash($c, PASSWORD_BCRYPT, ['cost' => 10]), $backupCodes);
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE users SET totp_enabled = 1, totp_backup_codes = ? WHERE id = ?');
|
||||
$stmt->execute([json_encode($hashedCodes), $userId]);
|
||||
|
||||
AuditLog::logUpdate('user', $userId, ['totp_enabled' => 0], ['totp_enabled' => 1], 'Uživatel aktivoval 2FA');
|
||||
|
||||
successResponse([
|
||||
'backup_codes' => $backupCodes,
|
||||
], '2FA bylo úspěšně aktivováno');
|
||||
}
|
||||
|
||||
/** POST ?action=disable { "code": "123456" } */
|
||||
function handleDisable(PDO $pdo, TwoFactorAuth $tfa): void
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
errorResponse('Metoda není povolena', 405);
|
||||
}
|
||||
|
||||
$authData = JWTAuth::requireAuth();
|
||||
AuditLog::setUser($authData['user_id'], $authData['user']['username'] ?? 'unknown');
|
||||
$userId = $authData['user_id'];
|
||||
$input = getJsonInput();
|
||||
$code = trim($input['code'] ?? '');
|
||||
|
||||
if (empty($code)) {
|
||||
errorResponse('Ověřovací kód je povinný');
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare('SELECT totp_secret, totp_enabled FROM users WHERE id = ?');
|
||||
$stmt->execute([$userId]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if (!$user['totp_enabled']) {
|
||||
errorResponse('2FA není aktivní');
|
||||
}
|
||||
|
||||
$decryptedSecret = decryptTotpSecret($user['totp_secret']);
|
||||
if (!$tfa->verifyCode($decryptedSecret, $code)) {
|
||||
errorResponse('Neplatný ověřovací kód');
|
||||
}
|
||||
$stmt = $pdo->prepare(
|
||||
'UPDATE users SET totp_enabled = 0, totp_secret = NULL,
|
||||
totp_backup_codes = NULL WHERE id = ?'
|
||||
);
|
||||
$stmt->execute([$userId]);
|
||||
|
||||
AuditLog::logUpdate('user', $userId, ['totp_enabled' => 1], ['totp_enabled' => 0], 'Uživatel deaktivoval 2FA');
|
||||
|
||||
successResponse(null, '2FA bylo deaktivováno');
|
||||
}
|
||||
|
||||
/**
|
||||
* POST ?action=verify - overeni TOTP kodu pri loginu (pre-auth)
|
||||
* Body: { "login_token": "...", "code": "123456", "remember": false }
|
||||
*/
|
||||
function handleVerify(PDO $pdo, TwoFactorAuth $tfa): void
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
errorResponse('Metoda není povolena', 405);
|
||||
}
|
||||
|
||||
$rateLimiter = new RateLimiter();
|
||||
$rateLimiter->setFailClosed();
|
||||
$rateLimiter->enforce('totp_2fa', 5);
|
||||
|
||||
$input = getJsonInput();
|
||||
$loginToken = $input['login_token'] ?? '';
|
||||
$code = trim($input['code'] ?? '');
|
||||
$remember = (bool) ($input['remember'] ?? false);
|
||||
|
||||
if (empty($loginToken) || empty($code)) {
|
||||
errorResponse('Přihlašovací token a ověřovací kód jsou povinné');
|
||||
}
|
||||
|
||||
$tokenData = verifyLoginToken($pdo, $loginToken);
|
||||
if (!$tokenData) {
|
||||
errorResponse('Neplatný nebo expirovaný přihlašovací token. Přihlaste se znovu.', 401);
|
||||
}
|
||||
|
||||
$userId = $tokenData['user_id'];
|
||||
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT u.*, r.name as role_name, r.display_name as role_display_name
|
||||
FROM users u
|
||||
LEFT JOIN roles r ON u.role_id = r.id
|
||||
WHERE u.id = ? AND u.totp_enabled = 1
|
||||
');
|
||||
$stmt->execute([$userId]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if (!$user) {
|
||||
errorResponse('Uživatel nenalezen nebo 2FA není aktivní', 401);
|
||||
}
|
||||
|
||||
$decryptedSecret = decryptTotpSecret($user['totp_secret']);
|
||||
if (!$tfa->verifyCode($decryptedSecret, $code, 1)) {
|
||||
errorResponse('Neplatný ověřovací kód');
|
||||
}
|
||||
|
||||
deleteLoginToken($pdo, $loginToken);
|
||||
completeLogin($pdo, $user, $remember);
|
||||
}
|
||||
|
||||
/** POST ?action=backup_verify { "login_token": "...", "code": "XXXXXXXX", "remember": false } */
|
||||
function handleBackupVerify(PDO $pdo): void
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
errorResponse('Metoda není povolena', 405);
|
||||
}
|
||||
|
||||
$rateLimiter = new RateLimiter();
|
||||
$rateLimiter->setFailClosed();
|
||||
$rateLimiter->enforce('totp_2fa', 5);
|
||||
|
||||
$input = getJsonInput();
|
||||
$loginToken = $input['login_token'] ?? '';
|
||||
$code = strtoupper(trim($input['code'] ?? ''));
|
||||
$remember = (bool) ($input['remember'] ?? false);
|
||||
|
||||
if (empty($loginToken) || empty($code)) {
|
||||
errorResponse('Přihlašovací token a záložní kód jsou povinné');
|
||||
}
|
||||
|
||||
$tokenData = verifyLoginToken($pdo, $loginToken);
|
||||
if (!$tokenData) {
|
||||
errorResponse('Neplatný nebo expirovaný přihlašovací token. Přihlaste se znovu.', 401);
|
||||
}
|
||||
|
||||
$userId = $tokenData['user_id'];
|
||||
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT u.*, r.name as role_name, r.display_name as role_display_name
|
||||
FROM users u
|
||||
LEFT JOIN roles r ON u.role_id = r.id
|
||||
WHERE u.id = ? AND u.totp_enabled = 1
|
||||
');
|
||||
$stmt->execute([$userId]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if (!$user || !$user['totp_backup_codes']) {
|
||||
errorResponse('Uživatel nenalezen nebo nemá záložní kódy', 401);
|
||||
}
|
||||
|
||||
$hashedCodes = json_decode($user['totp_backup_codes'], true);
|
||||
$matched = false;
|
||||
$remainingCodes = [];
|
||||
|
||||
foreach ($hashedCodes as $hashed) {
|
||||
if (!$matched && password_verify($code, $hashed)) {
|
||||
$matched = true;
|
||||
} else {
|
||||
$remainingCodes[] = $hashed;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$matched) {
|
||||
errorResponse('Neplatný záložní kód');
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE users SET totp_backup_codes = ? WHERE id = ?');
|
||||
$stmt->execute([json_encode($remainingCodes), $userId]);
|
||||
|
||||
deleteLoginToken($pdo, $loginToken);
|
||||
completeLogin($pdo, $user, $remember);
|
||||
}
|
||||
|
||||
/** GET ?action=get_required (admin only) */
|
||||
function handleGetRequired(PDO $pdo): void
|
||||
{
|
||||
$authData = JWTAuth::requireAuth();
|
||||
AuditLog::setUser($authData['user_id'], $authData['user']['username'] ?? 'unknown');
|
||||
requirePermission($authData, 'settings.security');
|
||||
|
||||
$stmt = $pdo->query("SELECT require_2fa FROM company_settings LIMIT 1");
|
||||
|
||||
successResponse([
|
||||
'require_2fa' => (bool) $stmt->fetchColumn(),
|
||||
]);
|
||||
}
|
||||
|
||||
/** POST ?action=set_required { "required": true/false } (admin only) */
|
||||
function handleSetRequired(PDO $pdo): void
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
errorResponse('Metoda není povolena', 405);
|
||||
}
|
||||
|
||||
$authData = JWTAuth::requireAuth();
|
||||
AuditLog::setUser($authData['user_id'], $authData['user']['username'] ?? 'unknown');
|
||||
requirePermission($authData, 'settings.security');
|
||||
|
||||
$input = getJsonInput();
|
||||
$required = (bool) ($input['required'] ?? false);
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE company_settings SET require_2fa = ? LIMIT 1");
|
||||
$stmt->execute([$required ? 1 : 0]);
|
||||
|
||||
successResponse([
|
||||
'require_2fa' => $required,
|
||||
], $required ? '2FA je nyní povinná pro všechny uživatele' : '2FA již není povinná');
|
||||
}
|
||||
|
||||
// --- Helper functions ---
|
||||
|
||||
/** Desifrovat TOTP secret z DB (zpetne kompatibilni s plaintextem pred migraci) */
|
||||
function decryptTotpSecret(string $value): string
|
||||
{
|
||||
if (Encryption::isEncrypted($value)) {
|
||||
return Encryption::decrypt($value);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generovat 8 nahodnych backup kodu
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
function generateBackupCodes(int $count = 8): array
|
||||
{
|
||||
$codes = [];
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$codes[] = strtoupper(bin2hex(random_bytes(4))); // 8-char hex
|
||||
}
|
||||
return $codes;
|
||||
}
|
||||
|
||||
/** Docasny login token pro 2FA (5 min) */
|
||||
function createLoginToken(PDO $pdo, int $userId): string
|
||||
{
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$hashedToken = hash('sha256', $token);
|
||||
$expiresAt = date('Y-m-d H:i:s', time() + 300); // 5 minutes
|
||||
|
||||
$stmt = $pdo->prepare('DELETE FROM totp_login_tokens WHERE user_id = ? OR expires_at < NOW()');
|
||||
$stmt->execute([$userId]);
|
||||
|
||||
$stmt = $pdo->prepare('
|
||||
INSERT INTO totp_login_tokens (user_id, token_hash, expires_at)
|
||||
VALUES (?, ?, ?)
|
||||
');
|
||||
$stmt->execute([$userId, $hashedToken, $expiresAt]);
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overit login token
|
||||
*
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
function verifyLoginToken(PDO $pdo, string $token): ?array
|
||||
{
|
||||
$hashedToken = hash('sha256', $token);
|
||||
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT * FROM totp_login_tokens
|
||||
WHERE token_hash = ? AND expires_at > NOW()
|
||||
');
|
||||
$stmt->execute([$hashedToken]);
|
||||
return $stmt->fetch() ?: null;
|
||||
}
|
||||
|
||||
/** Smazat login token po pouziti */
|
||||
function deleteLoginToken(PDO $pdo, string $token): void
|
||||
{
|
||||
$hashedToken = hash('sha256', $token);
|
||||
$stmt = $pdo->prepare('DELETE FROM totp_login_tokens WHERE token_hash = ?');
|
||||
$stmt->execute([$hashedToken]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dokoncit login po uspesnem 2FA - vydat JWT + refresh token
|
||||
*
|
||||
* @param array<string, mixed> $user
|
||||
*/
|
||||
function completeLogin(PDO $pdo, array $user, bool $remember): void
|
||||
{
|
||||
$stmt = $pdo->prepare('
|
||||
UPDATE users SET failed_login_attempts = 0, locked_until = NULL, last_login = NOW()
|
||||
WHERE id = ?
|
||||
');
|
||||
$stmt->execute([$user['id']]);
|
||||
|
||||
$userData = [
|
||||
'id' => $user['id'],
|
||||
'username' => $user['username'],
|
||||
'email' => $user['email'],
|
||||
'first_name' => $user['first_name'],
|
||||
'last_name' => $user['last_name'],
|
||||
'role' => $user['role_name'] ?? null,
|
||||
'role_display' => $user['role_display_name'] ?? $user['role_name'] ?? null,
|
||||
'is_admin' => ($user['role_name'] ?? '') === 'admin',
|
||||
];
|
||||
|
||||
$accessToken = JWTAuth::generateAccessToken($userData);
|
||||
JWTAuth::generateRefreshToken($user['id'], $remember);
|
||||
|
||||
AuditLog::logLogin($user['id'], $user['username']);
|
||||
|
||||
$stmt = $pdo->query("SELECT require_2fa FROM company_settings LIMIT 1");
|
||||
$require2FA = (bool) $stmt->fetchColumn();
|
||||
|
||||
successResponse([
|
||||
'access_token' => $accessToken,
|
||||
'expires_in' => JWTAuth::getAccessTokenExpiry(),
|
||||
'user' => [
|
||||
'id' => $userData['id'],
|
||||
'username' => $userData['username'],
|
||||
'email' => $userData['email'],
|
||||
'full_name' => trim($userData['first_name'] . ' ' . $userData['last_name']),
|
||||
'role' => $userData['role'],
|
||||
'role_display' => $userData['role_display'],
|
||||
'is_admin' => $userData['is_admin'],
|
||||
'permissions' => JWTAuth::getUserPermissions($user['id']),
|
||||
'totp_enabled' => true,
|
||||
'require_2fa' => $require2FA,
|
||||
],
|
||||
], 'Přihlášení úspěšné');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user