Files
app/api/admin/login.php
2026-03-12 12:43:56 +01:00

181 lines
5.8 KiB
PHP

<?php
/**
* BOHA Automation - Admin Login API (JWT)
*
* POST /api/admin/login.php
*
* Request body:
* {
* "username": "string",
* "password": "string",
* "remember": boolean (optional)
* }
*
* Response:
* {
* "success": boolean,
* "data": { "access_token", "expires_in", "user" } | null,
* "error": "string" | null
* }
*/
declare(strict_types=1);
require_once dirname(__DIR__) . '/config.php';
require_once dirname(__DIR__) . '/includes/JWTAuth.php';
require_once dirname(__DIR__) . '/includes/AuditLog.php';
require_once dirname(__DIR__) . '/includes/RateLimiter.php';
setCorsHeaders();
setSecurityHeaders();
setNoCacheHeaders();
header('Content-Type: application/json; charset=utf-8');
$rateLimiter = new RateLimiter();
$rateLimiter->setFailClosed();
$rateLimiter->enforce('login', 10);
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
errorResponse('Metoda není povolena', 405);
}
$input = getJsonInput();
$username = trim($input['username'] ?? '');
$password = $input['password'] ?? '';
$remember = (bool) ($input['remember'] ?? false);
if (empty($username)) {
errorResponse('Uživatelské jméno je povinné');
}
if (empty($password)) {
errorResponse('Heslo je povinné');
}
try {
$pdo = db();
$stmt = $pdo->prepare('
SELECT u.id, u.username, u.email, u.password_hash, u.first_name, u.last_name,
u.role_id, u.failed_login_attempts, u.locked_until, u.is_active, u.totp_enabled,
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.username = ? OR u.email = ?
');
$stmt->execute([$username, $username]);
$user = $stmt->fetch();
if (!$user) {
AuditLog::logLoginFailed($username, 'invalid_credentials');
errorResponse('Neplatné uživatelské jméno nebo heslo', 401);
}
if (!$user['is_active']) {
AuditLog::logLoginFailed($username, 'account_deactivated');
errorResponse('Neplatné uživatelské jméno nebo heslo', 401);
}
if ($user['locked_until'] && strtotime($user['locked_until']) > time()) {
AuditLog::logLoginFailed($username, 'account_locked');
errorResponse('Neplatné uživatelské jméno nebo heslo', 401);
}
if (!password_verify($password, $user['password_hash'])) {
$attempts = $user['failed_login_attempts'] + 1;
$lockUntil = null;
if ($attempts >= MAX_LOGIN_ATTEMPTS) {
$lockUntil = date('Y-m-d H:i:s', time() + (LOCKOUT_MINUTES * 60));
$attempts = 0; // Reset after lockout
}
$stmt = $pdo->prepare('
UPDATE users SET failed_login_attempts = ?, locked_until = ?
WHERE id = ?
');
$stmt->execute([$attempts, $lockUntil, $user['id']]);
AuditLog::logLoginFailed($username, 'invalid_credentials');
errorResponse('Neplatné uživatelské jméno nebo heslo', 401);
}
$role = ['name' => $user['role_name'], 'display_name' => $user['role_display_name']];
// 2FA - neresit failed_attempts, az po overeni
if ($user['totp_enabled']) {
$loginToken = bin2hex(random_bytes(32));
$hashedLoginToken = hash('sha256', $loginToken);
$loginTokenExpiry = date('Y-m-d H:i:s', time() + 300);
$stmt = $pdo->prepare('DELETE FROM totp_login_tokens WHERE user_id = ? OR expires_at < NOW()');
$stmt->execute([$user['id']]);
$stmt = $pdo->prepare('
INSERT INTO totp_login_tokens (user_id, token_hash, expires_at)
VALUES (?, ?, ?)
');
$stmt->execute([$user['id'], $hashedLoginToken, $loginTokenExpiry]);
successResponse([
'requires_2fa' => true,
'login_token' => $loginToken,
]);
}
// Bez 2FA - reset failed attempts a pokracovat
$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' => $role['name'] ?? null,
'role_display' => $role['display_name'] ?? $role['name'] ?? null,
'is_admin' => ($role['name'] ?? '') === 'admin',
];
$accessToken = JWTAuth::generateAccessToken($userData);
JWTAuth::generateRefreshToken($user['id'], $remember);
AuditLog::logLogin($user['id'], $user['username']);
$require2FA = false;
try {
$stmt = $pdo->query("SELECT require_2fa FROM company_settings LIMIT 1");
$require2FA = (bool) $stmt->fetchColumn();
} catch (PDOException $e) {
}
$permissions = JWTAuth::getUserPermissions($user['id']);
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' => $permissions,
'totp_enabled' => false,
'require_2fa' => $require2FA,
],
], 'Přihlášení úspěšné');
} catch (PDOException $e) {
error_log('Login PDO error: ' . $e->getMessage());
errorResponse('Došlo k systémové chybě. Zkuste to prosím později.', 500);
} catch (Exception $e) {
error_log('Login error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
errorResponse('Došlo k systémové chybě. Zkuste to prosím později.', 500);
}