Initial commit
This commit is contained in:
180
api/admin/login.php
Normal file
180
api/admin/login.php
Normal file
@@ -0,0 +1,180 @@
|
||||
<?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);
|
||||
}
|
||||
Reference in New Issue
Block a user