- Email 2FA is now optional and disabled by default.
Existing and new accounts default to 2FA off. Registration signs users in directly. Users can enable it under My Account. Administrators can manage it under Users & Staff. Verified password-only and opted-in email-code login flows. PHP/JavaScript checks and browser console passed.
This commit is contained in:
@@ -398,6 +398,7 @@ final class AdminController extends BaseController
|
||||
if ($id > 0) {
|
||||
$this->db->execute(
|
||||
'UPDATE users SET full_name = ?, phone = ?, role = ?, active = ?, newsletter_opt_in = ?,
|
||||
two_factor_enabled = ?,
|
||||
updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
||||
[
|
||||
Security::clean((string) ($_POST['full_name'] ?? '')),
|
||||
@@ -405,6 +406,7 @@ final class AdminController extends BaseController
|
||||
$role,
|
||||
isset($_POST['active']) ? 1 : 0,
|
||||
isset($_POST['newsletter_opt_in']) ? 1 : 0,
|
||||
isset($_POST['two_factor_enabled']) ? 1 : 0,
|
||||
$id,
|
||||
]
|
||||
);
|
||||
@@ -417,8 +419,9 @@ final class AdminController extends BaseController
|
||||
}
|
||||
$this->db->execute(
|
||||
'INSERT INTO users
|
||||
(email, password_hash, full_name, phone, role, active, newsletter_opt_in, email_verified_at)
|
||||
VALUES (?, ?, ?, ?, ?, 1, ?, CURRENT_TIMESTAMP)',
|
||||
(email, password_hash, full_name, phone, role, active, newsletter_opt_in,
|
||||
two_factor_enabled, email_verified_at)
|
||||
VALUES (?, ?, ?, ?, ?, 1, ?, ?, CURRENT_TIMESTAMP)',
|
||||
[
|
||||
$email,
|
||||
password_hash($password, PASSWORD_DEFAULT),
|
||||
@@ -426,6 +429,7 @@ final class AdminController extends BaseController
|
||||
Security::clean((string) ($_POST['phone'] ?? '')),
|
||||
$role,
|
||||
isset($_POST['newsletter_opt_in']) ? 1 : 0,
|
||||
isset($_POST['two_factor_enabled']) ? 1 : 0,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,15 +55,27 @@ final class AuthController extends BaseController
|
||||
Http::redirect('/login');
|
||||
}
|
||||
|
||||
unset($_SESSION['pending_verification_purpose']);
|
||||
$_SESSION['pending_2fa_user_id'] = (int) $user['id'];
|
||||
$developmentCode = $this->twoFactor->issue($user, '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.');
|
||||
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');
|
||||
}
|
||||
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
|
||||
@@ -137,13 +149,13 @@ final class AuthController extends BaseController
|
||||
]
|
||||
);
|
||||
$user = $this->db->fetch('SELECT * FROM users WHERE email = ?', [$clean['email']]);
|
||||
$_SESSION['pending_2fa_user_id'] = (int) $user['id'];
|
||||
$_SESSION['pending_verification_purpose'] = 'register';
|
||||
$developmentCode = $this->twoFactor->issue($user, 'register');
|
||||
if ($developmentCode !== null) {
|
||||
Http::flash('info', 'Development mail mode: your verification code is ' . $developmentCode . '.');
|
||||
if ($user === null) {
|
||||
Http::flash('error', 'Your account could not be loaded after registration.');
|
||||
Http::redirect('/login');
|
||||
}
|
||||
Http::redirect('/verify');
|
||||
$this->auth->login($user);
|
||||
Http::flash('success', 'Welcome, ' . $user['full_name'] . '. Your account is ready.');
|
||||
Http::redirect('/account');
|
||||
}
|
||||
|
||||
public function verifyForm(): void
|
||||
@@ -170,10 +182,10 @@ final class AuthController extends BaseController
|
||||
if ($user === null) {
|
||||
Http::redirect('/login');
|
||||
}
|
||||
if ($purpose === 'register') {
|
||||
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']);
|
||||
}
|
||||
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');
|
||||
@@ -205,6 +217,7 @@ final class AuthController extends BaseController
|
||||
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 !== ''
|
||||
@@ -220,7 +233,8 @@ final class AuthController extends BaseController
|
||||
|
||||
$this->db->execute(
|
||||
'UPDATE users SET full_name = ?, phone = ?, street_address = ?, city = ?, state = ?,
|
||||
postal_code = ?, newsletter_opt_in = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
||||
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'] ?? '')),
|
||||
@@ -229,6 +243,7 @@ final class AuthController extends BaseController
|
||||
strtoupper(substr(Security::clean((string) ($_POST['state'] ?? 'WV')), 0, 2)),
|
||||
Security::clean((string) ($_POST['postal_code'] ?? '')),
|
||||
$newsletter,
|
||||
$twoFactorEnabled,
|
||||
(int) $user['id'],
|
||||
]
|
||||
);
|
||||
|
||||
+2
-3
@@ -40,14 +40,14 @@ final class Auth
|
||||
{
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['user_id'] = (int) $user['id'];
|
||||
unset($_SESSION['pending_2fa_user_id']);
|
||||
unset($_SESSION['pending_2fa_user_id'], $_SESSION['pending_verification_purpose']);
|
||||
$this->resolved = true;
|
||||
$this->cachedUser = $user;
|
||||
}
|
||||
|
||||
public function logout(): void
|
||||
{
|
||||
unset($_SESSION['user_id'], $_SESSION['pending_2fa_user_id']);
|
||||
unset($_SESSION['user_id'], $_SESSION['pending_2fa_user_id'], $_SESSION['pending_verification_purpose']);
|
||||
session_regenerate_id(true);
|
||||
$this->resolved = true;
|
||||
$this->cachedUser = null;
|
||||
@@ -81,4 +81,3 @@ final class Auth
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
postal_code TEXT NOT NULL DEFAULT '',
|
||||
role TEXT NOT NULL DEFAULT 'customer',
|
||||
newsletter_opt_in INTEGER NOT NULL DEFAULT 1,
|
||||
two_factor_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
email_verified_at TEXT,
|
||||
active INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
@@ -264,9 +265,22 @@ CREATE INDEX IF NOT EXISTS idx_users_newsletter ON users(newsletter_opt_in, acti
|
||||
SQL;
|
||||
|
||||
$this->pdo->exec($schema);
|
||||
$this->ensureSqliteColumn('users', 'two_factor_enabled', 'INTEGER NOT NULL DEFAULT 0');
|
||||
$this->seed();
|
||||
}
|
||||
|
||||
private function ensureSqliteColumn(string $table, string $column, string $definition): void
|
||||
{
|
||||
$columns = $this->pdo->query("PRAGMA table_info(`{$table}`)")->fetchAll();
|
||||
foreach ($columns as $existing) {
|
||||
if (($existing['name'] ?? '') === $column) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->pdo->exec("ALTER TABLE `{$table}` ADD COLUMN `{$column}` {$definition}");
|
||||
}
|
||||
|
||||
private function seed(): void
|
||||
{
|
||||
$categoryCount = (int) $this->pdo->query('SELECT COUNT(*) FROM menu_categories')->fetchColumn();
|
||||
|
||||
Reference in New Issue
Block a user