user = new User(); $this->limiter = new RateLimiter(); } public function registerForm(): void { $content = __DIR__ . '/../Views/auth/register.php'; include __DIR__ . '/../Views/layouts/main.php'; } public function register(): void { if (!Csrf::validate($_POST['csrf_token'] ?? '')) { Flash::set('error', 'Invalid CSRF token.'); header('Location: /register'); return; } $username = trim($_POST['username'] ?? ''); $email = trim($_POST['email'] ?? ''); $password = $_POST['password'] ?? ''; $passwordConfirm = $_POST['password_confirm'] ?? ''; // Validation $errors = []; if ($username === '' || $email === '' || $password === '') { $errors[] = 'All fields are required.'; } if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $username)) { $errors[] = 'Username must be 3-20 characters (letters, numbers, underscores).'; } if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { $errors[] = 'Invalid email address.'; } if (\strlen($password) < 8) { $errors[] = 'Password must be at least 8 characters.'; } if ($password !== $passwordConfirm) { $errors[] = 'Passwords do not match.'; } if ($this->user->findByUsername($username)) { $errors[] = 'Username is already taken.'; } if ($this->user->findByEmail($email)) { $errors[] = 'Email is already registered.'; } if ($errors) { Flash::set('error', implode(' ', $errors)); header('Location: /register'); return; } $userId = $this->user->create($username, $email, $password); $token = $this->user->getVerificationToken($userId); Mail::sendVerification($email, $token); Flash::set('success', 'Account created! Check your email to verify.'); header('Location: /login'); } public function verify(): void { $token = $_GET['token'] ?? ''; $user = $this->user->findByVerificationToken($token); if (!$user) { Flash::set('error', 'Invalid or expired verification link.'); header('Location: /login'); return; } $this->user->verify($user['id']); Flash::set('success', 'Email verified! You can now log in.'); header('Location: /login'); } public function loginForm(): void { $content = __DIR__ . '/../Views/auth/login.php'; include __DIR__ . '/../Views/layouts/main.php'; } public function login(): void { if (!Csrf::validate($_POST['csrf_token'] ?? '')) { Flash::set('error', 'Invalid CSRF token.'); header('Location: /login'); return; } $ip = $_SERVER['REMOTE_ADDR'] ?? ''; if ($this->limiter->isLimited($ip, 'login', self::LOGIN_MAX_ATTEMPTS, self::LOGIN_WINDOW)) { Flash::set('error', 'Too many login attempts. Please try again later.'); header('Location: /login'); return; } $username = trim($_POST['username'] ?? ''); $password = $_POST['password'] ?? ''; $user = $this->user->findByUsername($username); // Use the same error message for wrong username or wrong password // to avoid revealing which usernames exist (user enumeration) if (!$user || !password_verify($password, $user['password_hash'])) { // Only record failed attempts — successful logins don't count $this->limiter->record($ip, 'login'); Flash::set('error', 'Invalid username or password.'); header('Location: /login'); return; } if (!$user['is_verified']) { Flash::set('error', 'Please verify your email before logging in.'); header('Location: /login'); return; } // Regenerate session ID to prevent session fixation attacks session_regenerate_id(true); $_SESSION['user_id'] = $user['id']; $_SESSION['username'] = $user['username']; header('Location: /'); } public function logout(): void { session_destroy(); header('Location: /'); } public function forgotPasswordForm(): void { $content = __DIR__ . '/../Views/auth/forgot-password.php'; include __DIR__ . '/../Views/layouts/main.php'; } public function forgotPassword(): void { if (!Csrf::validate($_POST['csrf_token'] ?? '')) { Flash::set('error', 'Invalid CSRF token.'); header('Location: /forgot-password'); return; } $ip = $_SERVER['REMOTE_ADDR'] ?? ''; if ($this->limiter->isLimited($ip, 'reset', self::RESET_MAX_ATTEMPTS, self::RESET_WINDOW)) { Flash::set('error', 'Too many reset requests. Please try again later.'); header('Location: /forgot-password'); return; } // Record every attempt — even for non-existent emails, to prevent // an attacker from probing email addresses at high speed $this->limiter->record($ip, 'reset'); $email = trim($_POST['email'] ?? ''); // Always show the same message whether the email exists or not // to prevent user enumeration Flash::set('success', 'If that email is registered, a reset link has been sent.'); $user = $this->user->findByEmail($email); if ($user) { $token = $this->user->setResetToken($user['id']); Mail::sendPasswordReset($email, $token); } header('Location: /login'); } public function resetPasswordForm(): void { $token = $_GET['token'] ?? ''; $user = $this->user->findByResetToken($token); if (!$user) { Flash::set('error', 'Invalid or expired reset link.'); header('Location: /login'); return; } $content = __DIR__ . '/../Views/auth/reset-password.php'; include __DIR__ . '/../Views/layouts/main.php'; } public function resetPassword(): void { if (!Csrf::validate($_POST['csrf_token'] ?? '')) { Flash::set('error', 'Invalid CSRF token.'); header('Location: /login'); return; } $token = $_POST['token'] ?? ''; $password = $_POST['password'] ?? ''; $passwordConfirm = $_POST['password_confirm'] ?? ''; $user = $this->user->findByResetToken($token); if (!$user) { Flash::set('error', 'Invalid or expired reset link.'); header('Location: /login'); return; } if (\strlen($password) < 8) { Flash::set('error', 'Password must be at least 8 characters.'); header('Location: /reset-password?token=' . urlencode($token)); return; } if ($password !== $passwordConfirm) { Flash::set('error', 'Passwords do not match.'); header('Location: /reset-password?token=' . urlencode($token)); return; } $this->user->updatePassword($user['id'], $password); Flash::set('success', 'Password updated! You can now log in.'); header('Location: /login'); } }