diff options
| author | Thomas Vanbesien <tvanbesi@proton.me> | 2026-03-21 21:35:51 +0100 |
|---|---|---|
| committer | Thomas Vanbesien <tvanbesi@proton.me> | 2026-03-21 21:35:51 +0100 |
| commit | bc54c8c31e7f50a7a365f9b4d22fe8c74a29f61a (patch) | |
| tree | 73a88384b9e472386d244119a0b4e4aa028c8b32 /src/app/Controllers/AuthController.php | |
| parent | d1ef15fa39935bfa0420c5ac2b8c269e294c9a6d (diff) | |
| download | camagru-bc54c8c31e7f50a7a365f9b4d22fe8c74a29f61a.tar.gz camagru-bc54c8c31e7f50a7a365f9b4d22fe8c74a29f61a.zip | |
Add user authentication with email verification and password reset
Implements registration, login/logout, email verification via token,
and password reset flow. Includes CSRF protection, flash messages,
MailPit for dev email testing, and security docs in README.
Diffstat (limited to 'src/app/Controllers/AuthController.php')
| -rw-r--r-- | src/app/Controllers/AuthController.php | 229 |
1 files changed, 229 insertions, 0 deletions
diff --git a/src/app/Controllers/AuthController.php b/src/app/Controllers/AuthController.php new file mode 100644 index 0000000..ae94295 --- /dev/null +++ b/src/app/Controllers/AuthController.php @@ -0,0 +1,229 @@ +<?php + +declare(strict_types=1); +// Handles authentication: register, login, logout, email verification, and password reset. + +namespace App\Controllers; + +use App\Csrf; +use App\Flash; +use App\Mail; +use App\Models\User; + +class AuthController +{ + private User $user; + + public function __construct() + { + $this->user = new User(); + } + + // ── Registration ── + + 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'); + } + + // ── Email verification ── + + 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'); + } + + // ── Login / Logout ── + + 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; + } + + $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'])) { + 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: /'); + } + + // ── Password reset ── + + 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; + } + + $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'); + } +} |
