Spaces:
Running
Running
File size: 8,784 Bytes
c7257f7 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 |
<?php
require_once 'config.php';
// Only handle POST requests
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['message' => 'Method not allowed']);
exit;
}
// Support both JSON (AJAX) and form-encoded POSTs. Prefer JSON when Content-Type is application/json.
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
if (stripos($contentType, 'application/json') !== false) {
$input = json_decode(file_get_contents('php://input'), true);
$username = htmlspecialchars(trim($input['username'] ?? ''), ENT_QUOTES, 'UTF-8');
$password = $input['password'] ?? '';
$is_form = false;
} else {
// form submission (application/x-www-form-urlencoded or multipart)
$username = htmlspecialchars(trim($_POST['username'] ?? ''), ENT_QUOTES, 'UTF-8');
$password = $_POST['password'] ?? '';
$is_form = true;
}
// Basic validation
if (empty($username) || empty($password)) {
if ($is_form) {
// Redirect back with error flag for simple form UX
header('Location: admin_login.php?error=1');
exit;
}
http_response_code(400);
echo json_encode(['message' => 'Username and password required']);
exit;
}
// Get client IP
$ip_address = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? 'unknown';
// Ensure login_attempts table exists (idempotent)
$pdo->exec(
"CREATE TABLE IF NOT EXISTS login_attempts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT,
ip_address TEXT,
attempted_at DATETIME DEFAULT (datetime('now')),
success INTEGER DEFAULT 0
);"
);
// DEVELOPMENT: allow temporary bypass of rate-limiting by creating a file named
// DISABLE_RATE_LIMIT in the project root. This is handy when resetting or bootstrapping
// the admin account. Remove the file to re-enable rate limiting.
$bypass_rate_limit = false;
if (file_exists(__DIR__ . '/DISABLE_RATE_LIMIT')) {
$bypass_rate_limit = true;
}
// Check rate limits (5 attempts per email in 15 minutes, 10 attempts per IP in 15 minutes)
$now = date('Y-m-d H:i:s');
$fifteen_minutes_ago = date('Y-m-d H:i:s', strtotime('-15 minutes'));
// Check username rate limit (skip if bypass enabled)
if (!$bypass_rate_limit) {
$stmt = $pdo->prepare("SELECT COUNT(*) as attempts FROM login_attempts WHERE email = ? AND attempted_at > ? AND success = 0");
$stmt->execute([$username, $fifteen_minutes_ago]);
$username_attempts = $stmt->fetch()['attempts'];
if ($username_attempts >= 5) {
// Record failed attempt
$stmt = $pdo->prepare("INSERT INTO login_attempts (email, ip_address, success) VALUES (?, ?, 0)");
$stmt->execute([$username, $ip_address]);
http_response_code(429);
echo json_encode(['message' => 'Too many failed attempts for this username. Try again in 15 minutes.', 'retry_after' => 900]);
exit;
}
}
// Check IP rate limit (skip if bypass enabled)
if (!$bypass_rate_limit) {
$stmt = $pdo->prepare("SELECT COUNT(*) as attempts FROM login_attempts WHERE ip_address = ? AND attempted_at > ? AND success = 0");
$stmt->execute([$ip_address, $fifteen_minutes_ago]);
$ip_attempts = $stmt->fetch()['attempts'];
if ($ip_attempts >= 10) {
// Record failed attempt
$stmt = $pdo->prepare("INSERT INTO login_attempts (email, ip_address, success) VALUES (?, ?, 0)");
$stmt->execute([$username, $ip_address]);
http_response_code(429);
echo json_encode(['message' => 'Too many failed attempts from this IP. Try again in 15 minutes.', 'retry_after' => 900]);
exit;
}
}
// Find user (do not assume `is_active` column exists in older DB schema)
$stmt = $pdo->prepare("SELECT * FROM admin_users WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
// If the `is_active` column exists, enforce it; otherwise assume active by default
if ($user && isset($user['is_active']) && (int)$user['is_active'] !== 1) {
// Record failed attempt
$stmt = $pdo->prepare("INSERT INTO login_attempts (email, ip_address, success) VALUES (?, ?, 0)");
$stmt->execute([$username, $ip_address]);
http_response_code(401);
echo json_encode(['message' => 'Invalid credentials']);
exit;
}
if (!$user || !password_verify($password, $user['password_hash'])) {
// Record failed attempt
$stmt = $pdo->prepare("INSERT INTO login_attempts (email, ip_address, success) VALUES (?, ?, 0)");
$stmt->execute([$username, $ip_address]);
http_response_code(401);
echo json_encode(['message' => 'Invalid credentials']);
exit;
}
// Record successful attempt
$stmt = $pdo->prepare("INSERT INTO login_attempts (email, ip_address, success) VALUES (?, ?, 1)");
$stmt->execute([$username, $ip_address]);
// Generate JWT token (15 minutes expiry)
$payload = [
'user_id' => $user['id'],
'username' => $user['username'],
'email' => $user['email'] ?? '',
'role' => $user['role'] ?? 'admin',
'iat' => time(),
'exp' => time() + (15 * 60) // 15 minutes
];
// Use fully-qualified class name to avoid file-local "use" dependency issues
$jwt = \Firebase\JWT\JWT::encode($payload, JWT_SECRET, 'HS256');
// Set secure cookie (for refresh token - simplified for demo)
// Choose cookie options depending on whether the request appears to be HTTPS.
// When served over HTTPS (e.g., GitHub.dev preview), use Secure + SameSite=None so the
// cookie can be accepted in some embedded contexts. For local HTTP development we keep
// Secure=false and SameSite=Strict.
$is_https = false;
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
$is_https = true;
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
$is_https = true;
} elseif (!empty($_SERVER['HTTP_CF_VISITOR']) && strpos($_SERVER['HTTP_CF_VISITOR'], 'https') !== false) {
$is_https = true;
}
$cookie_options = [
'expires' => time() + (7 * 24 * 60 * 60), // 7 days
'path' => '/',
'domain' => '',
'secure' => $is_https, // require secure when over HTTPS
'httponly' => true,
'samesite' => $is_https ? 'None' : 'Strict'
];
// PHP's setcookie accepted an array of options since 7.3; this usage is compatible.
setcookie('refresh_token', $jwt, $cookie_options);
if ($is_form) {
// For browser form submissions, instead of a bare 302 (which may be followed inside an iframe
// or blocked by preview environments), return a small HTML page that attempts a top-level
// navigation. The Set-Cookie header has already been emitted above so the cookie will apply.
header('Content-Type: text/html; charset=utf-8');
$dashboard = 'backend.php';
echo "<!doctype html>\n";
echo "<html><head><meta charset=\"utf-8\"><title>Prijava uspešna</title>\n";
// Meta refresh as an extra fallback
echo "<meta http-equiv=\"refresh\" content=\"0;url={$dashboard}\">\n";
echo "</head><body>\n";
echo "<script>\n";
echo "try {\n";
echo " if (window.top && window.top !== window) {\n";
echo " window.top.location.replace('{$dashboard}');\n";
echo " } else {\n";
echo " window.location.replace('{$dashboard}');\n";
echo " }\n";
echo "} catch (e) {\n";
echo " try { window.location.replace('{$dashboard}'); } catch (e) { /* ignore */ }\n";
echo "}\n";
echo "</script>\n";
echo "<p>Prijava uspešna. Če se brskalnik ne preusmeri samodejno, <a href=\"{$dashboard}\">kliknite tukaj</a>.</p>\n";
// Large fallback button to open dashboard in a new tab or top window
echo "<div style=\"margin-top:16px;\">\n";
echo " <a href=\"{$dashboard}\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"display:inline-block;background:#f59e0b;color:#fff;padding:8px 12px;border-radius:6px;text-decoration:none\">Open dashboard in new tab</a>\n";
echo " <button id=\"openTopBtn\" style=\"margin-left:8px;padding:8px 12px;border-radius:6px;background:#ef4444;color:#fff;border:none;cursor:pointer\">Open dashboard (top)</button>\n";
echo "</div>\n";
echo "<script>document.getElementById('openTopBtn').addEventListener('click', function(){ try{ if(window.top && window.top !== window){ window.top.location.href='${dashboard}'; } else { window.location.href='${dashboard}'; } }catch(e){ window.open('${dashboard}','_blank'); } });</script>\n";
echo "</body></html>\n";
exit;
}
// Default: JSON response for API/AJAX clients
echo json_encode([
'message' => 'Login successful',
'token' => $jwt,
'expires_in' => 900, // 15 minutes
'user' => [
'id' => $user['id'],
'username' => $user['username'],
'email' => $user['email'] ?? '',
'role' => $user['role'] ?? 'admin'
]
]);
?> |