auth->check()) { Http::redirect('/account'); } View::render('auth/login', $this->shared([ 'title' => 'Sign In', 'captcha' => Security::newCaptcha(), ])); } public function login(): void { Security::verifyCsrf(); $input = $_POST; $email = strtolower(Security::clean((string) ($input['email'] ?? ''))); if (!Security::verifyCaptcha($input)) { Http::old(['email' => $email]); Http::flash('error', 'Please complete the anti-spam check.'); Http::redirect('/login'); } $user = $this->db->fetch('SELECT * FROM users WHERE email = ? AND active = 1', [$email]); if ($user === null || !password_verify((string) ($input['password'] ?? ''), (string) $user['password_hash'])) { usleep(350000); Http::old(['email' => $email]); Http::flash('error', 'The email address or password was not recognized.'); Http::redirect('/login'); } unset($_SESSION['pending_2fa_user_id'], $_SESSION['pending_verification_purpose']); if ((bool) ($user['two_factor_enabled'] ?? false)) { $_SESSION['pending_2fa_user_id'] = (int) $user['id']; try { $developmentCode = $this->twoFactor->issue($user, 'login'); } catch (\Throwable) { unset($_SESSION['pending_2fa_user_id']); Http::flash('error', 'We could not send your sign-in code. Please try again or ask an administrator to disable email 2FA for your account.'); Http::redirect('/login'); } if ($developmentCode !== null) { Http::flash('info', 'Development mail mode: your verification code is ' . $developmentCode . '.'); } else { Http::flash('success', 'We emailed you a six-digit verification code.'); } Http::redirect('/verify'); } $this->auth->login($user); Http::flash('success', 'Welcome, ' . $user['full_name'] . '.'); Http::redirect(in_array($user['role'], ['admin', 'manager', 'staff'], true) ? '/admin' : '/account'); } public function registerForm(): void { if ($this->auth->check()) { Http::redirect('/account'); } View::render('auth/register', $this->shared([ 'title' => 'Create Account', 'captcha' => Security::newCaptcha(), ])); } public function register(): void { Security::verifyCsrf(); $input = $_POST; $clean = [ 'email' => strtolower(Security::clean((string) ($input['email'] ?? ''))), 'full_name' => Security::clean((string) ($input['full_name'] ?? '')), 'phone' => Security::clean((string) ($input['phone'] ?? '')), 'street_address' => Security::clean((string) ($input['street_address'] ?? '')), 'city' => Security::clean((string) ($input['city'] ?? '')), 'state' => strtoupper(substr(Security::clean((string) ($input['state'] ?? 'WV')), 0, 2)), 'postal_code' => Security::clean((string) ($input['postal_code'] ?? '')), ]; if (!Security::verifyCaptcha($input)) { Http::old($clean); Http::flash('error', 'Please complete the anti-spam check.'); Http::redirect('/register'); } if ( !filter_var($clean['email'], FILTER_VALIDATE_EMAIL) || $clean['full_name'] === '' || $clean['phone'] === '' || $clean['street_address'] === '' || $clean['city'] === '' || $clean['state'] === '' || $clean['postal_code'] === '' || strlen((string) ($input['password'] ?? '')) < 10 ) { Http::old($clean); Http::flash('error', 'Complete your contact and address details, enter a valid email, and use a password of at least 10 characters.'); Http::redirect('/register'); } if (($input['password'] ?? '') !== ($input['password_confirmation'] ?? '')) { Http::old($clean); Http::flash('error', 'The password confirmation does not match.'); Http::redirect('/register'); } if ($this->db->fetch('SELECT id FROM users WHERE email = ?', [$clean['email']])) { Http::old($clean); Http::flash('error', 'An account already exists for that email address.'); Http::redirect('/login'); } $this->db->execute( 'INSERT INTO users (email, password_hash, full_name, phone, street_address, city, state, postal_code, newsletter_opt_in) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)', [ $clean['email'], password_hash((string) $input['password'], PASSWORD_DEFAULT), $clean['full_name'], $clean['phone'], $clean['street_address'], $clean['city'], $clean['state'], $clean['postal_code'], ] ); $user = $this->db->fetch('SELECT * FROM users WHERE email = ?', [$clean['email']]); if ($user === null) { Http::flash('error', 'Your account could not be loaded after registration.'); Http::redirect('/login'); } $this->auth->login($user); Http::flash('success', 'Welcome, ' . $user['full_name'] . '. Your account is ready.'); Http::redirect('/account'); } public function verifyForm(): void { if (empty($_SESSION['pending_2fa_user_id'])) { Http::redirect('/login'); } View::render('auth/verify', $this->shared([ 'title' => 'Verify Your Email', ])); } public function verify(): void { Security::verifyCsrf(); $userId = (int) ($_SESSION['pending_2fa_user_id'] ?? 0); $purpose = (string) ($_SESSION['pending_verification_purpose'] ?? 'login'); if (!$this->twoFactor->verify($userId, Security::clean((string) ($_POST['code'] ?? '')), $purpose)) { Http::flash('error', 'That code is invalid or has expired.'); Http::redirect('/verify'); } $user = $this->db->fetch('SELECT * FROM users WHERE id = ?', [$userId]); if ($user === null) { Http::redirect('/login'); } if ($purpose === 'register' || empty($user['email_verified_at'])) { $this->db->execute('UPDATE users SET email_verified_at = CURRENT_TIMESTAMP WHERE id = ?', [$userId]); } unset($_SESSION['pending_verification_purpose']); $this->auth->login($user); Http::flash('success', 'Welcome, ' . $user['full_name'] . '.'); Http::redirect(in_array($user['role'], ['admin', 'manager', 'staff'], true) ? '/admin' : '/account'); } public function logout(): void { Security::verifyCsrf(); $this->auth->logout(); Http::flash('success', 'You have been signed out.'); Http::redirect('/'); } public function account(): void { $user = $this->auth->requireLogin(); $orders = $this->db->fetchAll( 'SELECT * FROM orders WHERE user_id = ? ORDER BY placed_at DESC LIMIT 25', [(int) $user['id']] ); View::render('account/index', $this->shared([ 'title' => 'My Account', 'orders' => $orders, ])); } public function updateAccount(): void { Security::verifyCsrf(); $user = $this->auth->requireLogin(); $newsletter = isset($_POST['newsletter_opt_in']) ? 1 : 0; $twoFactorEnabled = isset($_POST['two_factor_enabled']) ? 1 : 0; $newPassword = (string) ($_POST['new_password'] ?? ''); if ( $newPassword !== '' && ( !password_verify((string) ($_POST['current_password'] ?? ''), (string) $user['password_hash']) || strlen($newPassword) < 10 || $newPassword !== (string) ($_POST['new_password_confirmation'] ?? '') ) ) { Http::flash('error', 'Password was not changed. Check your current password and use a matching new password of at least 10 characters.'); Http::redirect('/account'); } $this->db->execute( 'UPDATE users SET full_name = ?, phone = ?, street_address = ?, city = ?, state = ?, postal_code = ?, newsletter_opt_in = ?, two_factor_enabled = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [ Security::clean((string) ($_POST['full_name'] ?? '')), Security::clean((string) ($_POST['phone'] ?? '')), Security::clean((string) ($_POST['street_address'] ?? '')), Security::clean((string) ($_POST['city'] ?? '')), strtoupper(substr(Security::clean((string) ($_POST['state'] ?? 'WV')), 0, 2)), Security::clean((string) ($_POST['postal_code'] ?? '')), $newsletter, $twoFactorEnabled, (int) $user['id'], ] ); if ($newPassword !== '') { $this->db->execute( 'UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [password_hash($newPassword, PASSWORD_DEFAULT), (int) $user['id']] ); } Http::flash('success', 'Your account has been updated.'); Http::redirect('/account'); } public function unsubscribe(): void { $userId = (int) ($_GET['user'] ?? 0); $signature = (string) ($_GET['signature'] ?? ''); $expected = hash_hmac('sha256', (string) $userId, $this->newsletterKey()); if ($userId > 0 && hash_equals($expected, $signature)) { $this->db->execute('UPDATE users SET newsletter_opt_in = 0 WHERE id = ?', [$userId]); Http::flash('success', 'You have been unsubscribed from email news.'); } else { Http::flash('error', 'That unsubscribe link is invalid.'); } Http::redirect('/'); } private function newsletterKey(): string { return hash('sha256', (string) $this->config->get('app.key') . '|newsletter'); } }