WebOrbiton
v3.0.0.0

FlatlyPage

407 lines · 18.6 KB
  1. <?php
  2. require_once '../config.php';
  3. require_once __DIR__ . '/login_tracking.php';
  4. ​
  5. if (session_status() === PHP_SESSION_NONE) {
  6. session_start();
  7. }
  8. ​
  9. if (isset($_SESSION['admin_logged_in']) && $_SESSION['admin_logged_in'] === true) {
  10. header('Location: ' . BASE_URL . '/admin/dashboard.php');
  11. exit;
  12. }
  13. ​
  14. $error = '';
  15. $success = '';
  16. ​
  17. try {
  18. $loginTracker = new LoginTracker();
  19. $clientIP = $loginTracker->getClientIP();
  20. if ($loginTracker->isIPBanned($clientIP)) {
  21. http_response_code(403);
  22. die('<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Access Denied</title><style>body{font-family:Arial,sans-serif;background:#f1f5f9;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;}div{background:#fff;padding:40px;border-radius:12px;box-shadow:0 2px 8px rgba(0,0,0,.1);text-align:center;}h1{color:#dc2626;margin:0 0 10px;}p{color:#64748b;margin:0;}</style></head><body><div><h1>Access Denied</h1><p>Your IP address has been banned.</p></div></body></html>');
  23. }
  24. } catch (Exception $e) {
  25. error_log('Login tracker initialization failed: ' . $e->getMessage());
  26. $loginTracker = null;
  27. }
  28. ​
  29. function checkRateLimit() {
  30. $rateLimitFile = DATA_DIR . '/rate_limit.json';
  31. $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
  32. $now = time();
  33. $windowSeconds = 300; // 5 minutes
  34. $maxAttempts = 5;
  35. $rateLimits = [];
  36. if (file_exists($rateLimitFile)) {
  37. $rateLimits = json_decode(@file_get_contents($rateLimitFile), true) ?: [];
  38. }
  39. $rateLimits = array_filter($rateLimits, function($data) use ($now, $windowSeconds) {
  40. return ($now - $data['first_attempt']) < $windowSeconds;
  41. });
  42. if (isset($rateLimits[$ip])) {
  43. if ($rateLimits[$ip]['attempts'] >= $maxAttempts) {
  44. $timeLeft = $windowSeconds - ($now - $rateLimits[$ip]['first_attempt']);
  45. return [
  46. 'allowed' => false,
  47. 'timeLeft' => ceil($timeLeft / 60)
  48. ];
  49. }
  50. }
  51. return ['allowed' => true];
  52. }
  53. ​
  54. function recordLoginAttempt($success = false) {
  55. $rateLimitFile = DATA_DIR . '/rate_limit.json';
  56. $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
  57. $now = time();
  58. $rateLimits = [];
  59. if (file_exists($rateLimitFile)) {
  60. $rateLimits = json_decode(@file_get_contents($rateLimitFile), true) ?: [];
  61. }
  62. if ($success) {
  63. unset($rateLimits[$ip]);
  64. } else {
  65. if (!isset($rateLimits[$ip])) {
  66. $rateLimits[$ip] = [
  67. 'attempts' => 1,
  68. 'first_attempt' => $now
  69. ];
  70. } else {
  71. $rateLimits[$ip]['attempts']++;
  72. }
  73. }
  74. @file_put_contents($rateLimitFile, json_encode($rateLimits));
  75. }
  76. ​
  77. $adminFile = DATA_DIR . '/admin.json';
  78. $adminExists = file_exists($adminFile);
  79. ​
  80. if (!$adminExists && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'register') {
  81. $username = trim($_POST['username'] ?? '');
  82. $password = $_POST['password'] ?? '';
  83. $confirmPassword = $_POST['confirm_password'] ?? '';
  84. $email = strtolower(trim($_POST['email'] ?? ''));
  85. if (empty($username) || empty($password)) {
  86. $error = 'Please fill in all fields.';
  87. } elseif (strlen($password) < 8) {
  88. $error = 'Password must be at least 8 characters.';
  89. } elseif ($password !== $confirmPassword) {
  90. $error = 'Passwords do not match.';
  91. } elseif ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
  92. $error = 'Please enter a valid email address.';
  93. } else {
  94. $adminData = [
  95. 'username' => $username,
  96. 'password' => password_hash($password, PASSWORD_DEFAULT),
  97. 'email' => $email,
  98. 'created_at' => date('Y-m-d H:i:s')
  99. ];
  100. if (file_put_contents($adminFile, json_encode($adminData, JSON_PRETTY_PRINT))) {
  101. $success = 'Admin account created successfully. You can now log in.';
  102. $adminExists = true;
  103. } else {
  104. $error = 'Failed to create account. Check write permissions.';
  105. }
  106. }
  107. }
  108. ​
  109. if ($adminExists && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'login') {
  110. $rateLimitCheck = checkRateLimit();
  111. if (!$rateLimitCheck['allowed']) {
  112. $error = 'Too many login attempts. Please try again in ' . $rateLimitCheck['timeLeft'] . ' minute(s).';
  113. } else {
  114. $username = trim($_POST['username'] ?? '');
  115. $password = $_POST['password'] ?? '';
  116. if (empty($username) || empty($password)) {
  117. $error = 'Please fill in all fields.';
  118. recordLoginAttempt(false);
  119. } else {
  120. $adminData = json_decode(@file_get_contents($adminFile), true) ?: [];
  121. if ($adminData && $adminData['username'] === $username && password_verify($password, $adminData['password'])) {
  122. recordLoginAttempt(true);
  123. ​
  124. $adminEmail = $adminData['email'] ?? '';
  125. $lastLoginFile = DATA_DIR . '/lastlogin.json';
  126. $currentIP = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
  127. $currentUA = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
  128. $now = date('Y-m-d H:i:s');
  129. ​
  130. $prevLogin = [];
  131. if (file_exists($lastLoginFile)) {
  132. $prevLogin = json_decode(@file_get_contents($lastLoginFile), true) ?: [];
  133. }
  134. ​
  135. $shouldNotify = empty($prevLogin)
  136. || $prevLogin['ip'] !== $currentIP
  137. || $prevLogin['user_agent'] !== $currentUA;
  138. ​
  139. if ($adminEmail !== '' && $shouldNotify) {
  140. $alertTo = $adminEmail;
  141. $alertFrom = 'alert@flatlypage.com';
  142. $alertSubject = 'New login detected - FlatlyPage CMS';
  143. ​
  144. $currentIPSafe = htmlspecialchars($currentIP, ENT_QUOTES, 'UTF-8');
  145. $currentUASafe = htmlspecialchars($currentUA, ENT_QUOTES, 'UTF-8');
  146. $nowSafe = htmlspecialchars($now, ENT_QUOTES, 'UTF-8');
  147. ​
  148. $prevBlock = '';
  149. if (!empty($prevLogin)) {
  150. $prevIPSafe = htmlspecialchars($prevLogin['ip'] ?? '', ENT_QUOTES, 'UTF-8');
  151. $prevUASafe = htmlspecialchars($prevLogin['user_agent'] ?? '', ENT_QUOTES, 'UTF-8');
  152. $prevTimeSafe = htmlspecialchars($prevLogin['time'] ?? '', ENT_QUOTES, 'UTF-8');
  153. $prevBlock = "
  154. <tr><td colspan='2' style='padding:16px 0 4px;font-size:12px;color:#94a3b8;text-transform:uppercase;letter-spacing:.05em;'>Previous login</td></tr>
  155. <tr>
  156. <td style='padding:4px 0;color:#64748b;font-size:13px;'>IP address</td>
  157. <td style='padding:4px 0;color:#1e293b;font-size:13px;font-weight:500;'>{$prevIPSafe}</td>
  158. </tr>
  159. <tr>
  160. <td style='padding:4px 0;color:#64748b;font-size:13px;'>Device</td>
  161. <td style='padding:4px 0;color:#1e293b;font-size:13px;font-weight:500;'>{$prevUASafe}</td>
  162. </tr>
  163. <tr>
  164. <td style='padding:4px 0;color:#64748b;font-size:13px;'>Time</td>
  165. <td style='padding:4px 0;color:#1e293b;font-size:13px;font-weight:500;'>{$prevTimeSafe}</td>
  166. </tr>";
  167. }
  168. ​
  169. $alertHtml = "<!DOCTYPE html>
  170. <html><head><meta charset='UTF-8'></head>
  171. <body style='font-family:Arial,sans-serif;background:#f1f5f9;margin:0;padding:24px 0;'>
  172. <div style='max-width:520px;margin:0 auto;background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,.08);'>
  173. <div style='background:#1e293b;padding:28px 32px;'>
  174. <h1 style='margin:0;color:#fff;font-size:18px;font-weight:600;'>FlatlyPage CMS</h1>
  175. <p style='margin:4px 0 0;color:#94a3b8;font-size:13px;'>Security Notification</p>
  176. </div>
  177. <div style='padding:28px 32px 32px;'>
  178. <p style='margin:0 0 20px;font-size:15px;color:#1e293b;font-weight:600;'>A new login was detected on your account.</p>
  179. <table style='width:100%;border-collapse:collapse;'>
  180. <tr><td colspan='2' style='padding:0 0 4px;font-size:12px;color:#94a3b8;text-transform:uppercase;letter-spacing:.05em;'>Current login</td></tr>
  181. <tr>
  182. <td style='padding:4px 0;color:#64748b;font-size:13px;width:100px;'>IP address</td>
  183. <td style='padding:4px 0;color:#1e293b;font-size:13px;font-weight:500;'>{$currentIPSafe}</td>
  184. </tr>
  185. <tr>
  186. <td style='padding:4px 0;color:#64748b;font-size:13px;'>Device</td>
  187. <td style='padding:4px 0;color:#1e293b;font-size:13px;font-weight:500;'>{$currentUASafe}</td>
  188. </tr>
  189. <tr>
  190. <td style='padding:4px 0;color:#64748b;font-size:13px;'>Time (UTC)</td>
  191. <td style='padding:4px 0;color:#1e293b;font-size:13px;font-weight:500;'>{$nowSafe}</td>
  192. </tr>
  193. {$prevBlock}
  194. </table>
  195. <hr style='border:none;border-top:1px solid #e2e8f0;margin:24px 0 20px;'>
  196. <p style='margin:0;font-size:12px;color:#94a3b8;'>If you did not log in, secure your account immediately by changing your password.</p>
  197. </div>
  198. </div>
  199. </body></html>";
  200. ​
  201. $alertHeaders = "MIME-Version: 1.0\r\n";
  202. $alertHeaders .= "Content-type: text/html; charset=UTF-8\r\n";
  203. $alertHeaders .= "From: FlatlyPage CMS <{$alertFrom}>\r\n";
  204. $alertHeaders .= "Reply-To: {$alertFrom}\r\n";
  205. ​
  206. @mail($alertTo, $alertSubject, $alertHtml, $alertHeaders);
  207. }
  208. ​
  209. $newLogin = [
  210. 'ip' => $currentIP,
  211. 'user_agent' => $currentUA,
  212. 'time' => $now,
  213. ];
  214. @file_put_contents($lastLoginFile, json_encode($newLogin, JSON_PRETTY_PRINT));
  215. ​
  216. if ($loginTracker !== null) {
  217. try {
  218. $loginTracker->recordLogin($username);
  219. } catch (Exception $e) {
  220. error_log('Failed to record login in tracker: ' . $e->getMessage());
  221. }
  222. }
  223. ​
  224. $_SESSION['admin_logged_in'] = true;
  225. $_SESSION['admin_username'] = $username;
  226. $_SESSION['admin_login_time'] = time();
  227. ​
  228. session_regenerate_id(true);
  229. ​
  230. header('Location: ' . BASE_URL . '/admin/dashboard.php');
  231. exit;
  232. } else {
  233. $error = 'Invalid username or password.';
  234. recordLoginAttempt(false);
  235. }
  236. }
  237. }
  238. }
  239. ?>
  240. <!DOCTYPE html>
  241. <html lang="en">
  242. <head>
  243. <meta charset="UTF-8">
  244. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  245. <title><?php echo $adminExists ? 'Login' : 'Setup'; ?> - Admin Dashboard</title>
  246. <link rel="icon" href="/admin/admin.ico" type="image/x-icon">
  247. <script src="https://cdn.tailwindcss.com"></script>
  248. <?= admin_font_head() ?>
  249. <script src="/assets/js/admin-theme.js?v=5"></script>
  250. <style>
  251. * { font-family: var(--font-interface, 'Space Grotesk'), sans-serif; }
  252. </style>
  253. </head>
  254. <body class="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 flex items-center justify-center p-4">
  255. <div class="w-full max-w-md">
  256. <div class="text-center mb-8">
  257. <div class="inline-flex items-center justify-center w-16 h-16 bg-blue-600 rounded-2xl mb-4">
  258. <img src="/logos/flatlypage_light.svg" alt="Logo" class="w-10 h-10">
  259. </div>
  260. <h1 class="text-2xl font-bold text-white">FlatlyPage CMS</h1>
  261. <p class="text-slate-400 mt-2">
  262. <?php echo $adminExists ? 'Sign in to your account' : 'Create an admin account'; ?>
  263. </p>
  264. </div>
  265. ​
  266. <div class="bg-white rounded-2xl shadow-2xl p-8">
  267. <?php if ($error): ?>
  268. <div class="mb-6 p-4 bg-red-50 border border-red-200 rounded-xl">
  269. <div class="flex items-center gap-3">
  270. <svg class="w-5 h-5 text-red-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  271. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
  272. </svg>
  273. <p class="text-red-700 text-sm"><?php echo htmlspecialchars($error, ENT_QUOTES, 'UTF-8'); ?></p>
  274. </div>
  275. </div>
  276. <?php endif; ?>
  277. ​
  278. <?php if ($success): ?>
  279. <div class="mb-6 p-4 bg-green-50 border border-green-200 rounded-xl">
  280. <div class="flex items-center gap-3">
  281. <svg class="w-5 h-5 text-green-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  282. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
  283. </svg>
  284. <p class="text-green-700 text-sm"><?php echo htmlspecialchars($success, ENT_QUOTES, 'UTF-8'); ?></p>
  285. </div>
  286. </div>
  287. <?php endif; ?>
  288. ​
  289. <?php if (!$adminExists): ?>
  290. <form method="POST" action="" class="space-y-5">
  291. <input type="hidden" name="action" value="register">
  292. <div>
  293. <label for="username" class="block text-sm font-medium text-gray-700 mb-2">
  294. Username
  295. </label>
  296. <input type="text"
  297. id="username"
  298. name="username"
  299. required
  300. autocomplete="username"
  301. class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
  302. placeholder="Enter username">
  303. </div>
  304. ​
  305. <div>
  306. <label for="password" class="block text-sm font-medium text-gray-700 mb-2">
  307. Password
  308. </label>
  309. <input type="password"
  310. id="password"
  311. name="password"
  312. required
  313. minlength="8"
  314. autocomplete="new-password"
  315. class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
  316. placeholder="Minimum 8 characters">
  317. </div>
  318. ​
  319. <div>
  320. <label for="confirm_password" class="block text-sm font-medium text-gray-700 mb-2">
  321. Confirm Password
  322. </label>
  323. <input type="password"
  324. id="confirm_password"
  325. name="confirm_password"
  326. required
  327. minlength="8"
  328. autocomplete="new-password"
  329. class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
  330. placeholder="Confirm your password">
  331. </div>
  332. ​
  333. <div>
  334. <label for="reg_email" class="block text-sm font-medium text-gray-700 mb-2">
  335. Email <span class="text-gray-400 font-normal">(optional – login alerts)</span>
  336. </label>
  337. <input type="email"
  338. id="reg_email"
  339. name="email"
  340. autocomplete="email"
  341. class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
  342. placeholder="you@example.com">
  343. </div>
  344. ​
  345. <button type="submit"
  346. class="w-full py-3 px-4 bg-blue-600 text-white font-semibold rounded-xl hover:bg-blue-700 focus:ring-4 focus:ring-blue-200 transition-all">
  347. Create Account
  348. </button>
  349. </form>
  350. <?php else: ?>
  351. <form method="POST" action="" class="space-y-5">
  352. <input type="hidden" name="action" value="login">
  353. <div>
  354. <label for="username" class="block text-sm font-medium text-gray-700 mb-2">
  355. Username
  356. </label>
  357. <input type="text"
  358. id="username"
  359. name="username"
  360. required
  361. autocomplete="username"
  362. class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
  363. placeholder="Enter your username">
  364. </div>
  365. ​
  366. <div>
  367. <label for="password" class="block text-sm font-medium text-gray-700 mb-2">
  368. Password
  369. </label>
  370. <input type="password"
  371. id="password"
  372. name="password"
  373. required
  374. autocomplete="current-password"
  375. class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
  376. placeholder="Enter your password">
  377. </div>
  378. ​
  379. <button type="submit"
  380. class="w-full py-3 px-4 bg-blue-600 text-white font-semibold rounded-xl hover:bg-blue-700 focus:ring-4 focus:ring-blue-200 transition-all">
  381. Sign In
  382. </button>
  383. </form>
  384. <?php endif; ?>
  385. </div>
  386. ​
  387. <p class="text-center text-slate-500 text-sm mt-8">
  388. FlatlyPage CMS &copy; <?php echo date('Y'); ?>
  389. </p>
  390. </div>
  391. </body>
  392. </html>