Compare commits
3 Commits
7668ee2040
...
200f87ea48
Author | SHA1 | Date |
---|---|---|
|
200f87ea48 | |
|
7676bcd1c1 | |
|
925df9b915 |
|
@ -11,6 +11,7 @@ class User {
|
||||||
*/
|
*/
|
||||||
private $db;
|
private $db;
|
||||||
private $rateLimiter;
|
private $rateLimiter;
|
||||||
|
private $twoFactorAuth;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User constructor.
|
* User constructor.
|
||||||
|
@ -19,8 +20,16 @@ class User {
|
||||||
* @param object $database The database object to initialize the connection.
|
* @param object $database The database object to initialize the connection.
|
||||||
*/
|
*/
|
||||||
public function __construct($database) {
|
public function __construct($database) {
|
||||||
$this->db = $database->getConnection();
|
if ($database instanceof PDO) {
|
||||||
|
$this->db = $database;
|
||||||
|
} else {
|
||||||
|
$this->db = $database->getConnection();
|
||||||
|
}
|
||||||
|
require_once __DIR__ . '/ratelimiter.php';
|
||||||
|
require_once __DIR__ . '/twoFactorAuth.php';
|
||||||
|
|
||||||
$this->rateLimiter = new RateLimiter($database);
|
$this->rateLimiter = new RateLimiter($database);
|
||||||
|
$this->twoFactorAuth = new TwoFactorAuthentication($database);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -86,10 +95,11 @@ class User {
|
||||||
*
|
*
|
||||||
* @param string $username The username of the user.
|
* @param string $username The username of the user.
|
||||||
* @param string $password The password of the user.
|
* @param string $password The password of the user.
|
||||||
|
* @param string $twoFactorCode Optional. The 2FA code if 2FA is enabled.
|
||||||
*
|
*
|
||||||
* @return bool True if login is successful, false otherwise.
|
* @return array Login result with status and any necessary data
|
||||||
*/
|
*/
|
||||||
public function login($username, $password) {
|
public function login($username, $password, $twoFactorCode = null) {
|
||||||
// Get user's IP address
|
// Get user's IP address
|
||||||
require_once __DIR__ . '/../helpers/logs.php';
|
require_once __DIR__ . '/../helpers/logs.php';
|
||||||
$ipAddress = getUserIP();
|
$ipAddress = getUserIP();
|
||||||
|
@ -110,14 +120,41 @@ class User {
|
||||||
|
|
||||||
$user = $query->fetch(PDO::FETCH_ASSOC);
|
$user = $query->fetch(PDO::FETCH_ASSOC);
|
||||||
if ($user && password_verify($password, $user['password'])) {
|
if ($user && password_verify($password, $user['password'])) {
|
||||||
|
// Check if 2FA is enabled
|
||||||
|
if ($this->twoFactorAuth->isEnabled($user['id'])) {
|
||||||
|
if ($twoFactorCode === null) {
|
||||||
|
return [
|
||||||
|
'status' => 'requires_2fa',
|
||||||
|
'user_id' => $user['id'],
|
||||||
|
'username' => $user['username']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify 2FA code
|
||||||
|
if (!$this->twoFactorAuth->verify($user['id'], $twoFactorCode)) {
|
||||||
|
return [
|
||||||
|
'status' => 'invalid_2fa',
|
||||||
|
'message' => 'Invalid 2FA code'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login successful
|
||||||
$_SESSION['user_id'] = $user['id'];
|
$_SESSION['user_id'] = $user['id'];
|
||||||
$_SESSION['username'] = $user['username'];
|
$_SESSION['username'] = $user['username'];
|
||||||
return true;
|
return [
|
||||||
|
'status' => 'success',
|
||||||
|
'user_id' => $user['id'],
|
||||||
|
'username' => $user['username']
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get remaining attempts AFTER this failed attempt
|
// Get remaining attempts AFTER this failed attempt
|
||||||
$remainingAttempts = $this->rateLimiter->getRemainingAttempts($username, $ipAddress);
|
$remainingAttempts = $this->rateLimiter->getRemainingAttempts($username, $ipAddress);
|
||||||
throw new Exception("Invalid credentials. {$remainingAttempts} attempts remaining.");
|
return [
|
||||||
|
'status' => 'failed',
|
||||||
|
'message' => "Invalid credentials. {$remainingAttempts} attempts remaining."
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -450,4 +487,97 @@ class User {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all users for messaging
|
||||||
|
*
|
||||||
|
* @return array List of users with their IDs and usernames
|
||||||
|
*/
|
||||||
|
public function getUsers() {
|
||||||
|
$sql = "SELECT id, username
|
||||||
|
FROM users
|
||||||
|
ORDER BY username ASC";
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable two-factor authentication for a user
|
||||||
|
*
|
||||||
|
* @param int $userId User ID
|
||||||
|
* @return array Result of enabling 2FA
|
||||||
|
*/
|
||||||
|
public function enableTwoFactor($userId) {
|
||||||
|
return $this->twoFactorAuth->enable($userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable two-factor authentication for a user
|
||||||
|
*
|
||||||
|
* @param int $userId User ID
|
||||||
|
* @return bool True if disabled successfully
|
||||||
|
*/
|
||||||
|
public function disableTwoFactor($userId) {
|
||||||
|
return $this->twoFactorAuth->disable($userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify a two-factor authentication code
|
||||||
|
*
|
||||||
|
* @param int $userId User ID
|
||||||
|
* @param string $code The verification code
|
||||||
|
* @return bool True if verified
|
||||||
|
*/
|
||||||
|
public function verifyTwoFactor($userId, $code) {
|
||||||
|
return $this->twoFactorAuth->verify($userId, $code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if two-factor authentication is enabled for a user
|
||||||
|
*
|
||||||
|
* @param int $userId User ID
|
||||||
|
* @return bool True if enabled
|
||||||
|
*/
|
||||||
|
public function isTwoFactorEnabled($userId) {
|
||||||
|
return $this->twoFactorAuth->isEnabled($userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change a user's password
|
||||||
|
*
|
||||||
|
* @param int $userId User ID
|
||||||
|
* @param string $currentPassword Current password for verification
|
||||||
|
* @param string $newPassword New password to set
|
||||||
|
* @return bool True if password was changed successfully
|
||||||
|
*/
|
||||||
|
public function changePassword($userId, $currentPassword, $newPassword) {
|
||||||
|
try {
|
||||||
|
// First verify the current password
|
||||||
|
$sql = "SELECT password FROM users WHERE id = :user_id";
|
||||||
|
$query = $this->db->prepare($sql);
|
||||||
|
$query->execute([':user_id' => $userId]);
|
||||||
|
$user = $query->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$user || !password_verify($currentPassword, $user['password'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash the new password
|
||||||
|
$hashedPassword = password_hash($newPassword, PASSWORD_DEFAULT);
|
||||||
|
|
||||||
|
// Update the password
|
||||||
|
$sql = "UPDATE users SET password = :password WHERE id = :user_id";
|
||||||
|
$query = $this->db->prepare($sql);
|
||||||
|
return $query->execute([
|
||||||
|
':password' => $hashedPassword,
|
||||||
|
':user_id' => $userId
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("Error changing password: " . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,159 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User credentials management
|
||||||
|
*
|
||||||
|
* This page ("credentials") handles all credential-related actions including:
|
||||||
|
* - Two-factor authentication (2FA) setup, verification, and management
|
||||||
|
* - Password changes and resets
|
||||||
|
*
|
||||||
|
* Actions handled:
|
||||||
|
* - `setup`: Initial 2FA setup and verification
|
||||||
|
* - `verify`: Verify 2FA codes during login
|
||||||
|
* - `disable`: Disable 2FA
|
||||||
|
* - `password`: Change password
|
||||||
|
*/
|
||||||
|
|
||||||
|
$action = $_REQUEST['action'] ?? '';
|
||||||
|
$item = $_REQUEST['item'] ?? '';
|
||||||
|
|
||||||
|
// if a form is submitted
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||||
|
// Validate CSRF token
|
||||||
|
require_once '../app/helpers/security.php';
|
||||||
|
$security = SecurityHelper::getInstance();
|
||||||
|
if (!$security->verifyCsrfToken($_POST['csrf_token'] ?? '')) {
|
||||||
|
Feedback::flash('ERROR', 'DEFAULT', 'Invalid security token. Please try again.');
|
||||||
|
header("Location: $app_root?page=credentials");
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply rate limiting
|
||||||
|
require_once '../app/includes/rate_limit_middleware.php';
|
||||||
|
checkRateLimit($db, 'credentials', $user_id);
|
||||||
|
|
||||||
|
switch ($item) {
|
||||||
|
case '2fa':
|
||||||
|
require_once '../app/helpers/2fa.php';
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'setup':
|
||||||
|
// Validate the setup code
|
||||||
|
$code = $_POST['code'] ?? '';
|
||||||
|
$secret = $_POST['secret'] ?? '';
|
||||||
|
|
||||||
|
if ($userObject->enableTwoFactor($user_id, $secret, $code)) {
|
||||||
|
Feedback::flash('NOTICE', 'DEFAULT', 'Two-factor authentication has been enabled successfully.');
|
||||||
|
header("Location: $app_root?page=credentials");
|
||||||
|
exit();
|
||||||
|
} else {
|
||||||
|
Feedback::flash('ERROR', 'DEFAULT', 'Invalid verification code. Please try again.');
|
||||||
|
header("Location: $app_root?page=credentials&action=edit");
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'verify':
|
||||||
|
$code = $_POST['code'] ?? '';
|
||||||
|
if ($userObject->verifyTwoFactor($user_id, $code)) {
|
||||||
|
$_SESSION['2fa_verified'] = true;
|
||||||
|
header("Location: $app_root?page=dashboard");
|
||||||
|
exit();
|
||||||
|
} else {
|
||||||
|
Feedback::flash('ERROR', 'DEFAULT', 'Invalid verification code. Please try again.');
|
||||||
|
header("Location: $app_root?page=credentials&action=verify");
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'disable':
|
||||||
|
if ($userObject->disableTwoFactor($user_id)) {
|
||||||
|
Feedback::flash('NOTICE', 'DEFAULT', 'Two-factor authentication has been disabled.');
|
||||||
|
} else {
|
||||||
|
Feedback::flash('ERROR', 'DEFAULT', 'Failed to disable two-factor authentication.');
|
||||||
|
}
|
||||||
|
header("Location: $app_root?page=credentials");
|
||||||
|
exit();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'password':
|
||||||
|
require_once '../app/classes/validator.php';
|
||||||
|
|
||||||
|
$validator = new Validator($_POST);
|
||||||
|
$rules = [
|
||||||
|
'current_password' => [
|
||||||
|
'required' => true,
|
||||||
|
'min' => 8
|
||||||
|
],
|
||||||
|
'new_password' => [
|
||||||
|
'required' => true,
|
||||||
|
'min' => 8
|
||||||
|
],
|
||||||
|
'confirm_password' => [
|
||||||
|
'required' => true,
|
||||||
|
'matches' => 'new_password'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!$validator->validate($rules)) {
|
||||||
|
Feedback::flash('ERROR', 'DEFAULT', $validator->getFirstError());
|
||||||
|
header("Location: $app_root?page=credentials");
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($userObject->changePassword($user_id, $_POST['current_password'], $_POST['new_password'])) {
|
||||||
|
Feedback::flash('NOTICE', 'DEFAULT', 'Password has been changed successfully.');
|
||||||
|
} else {
|
||||||
|
Feedback::flash('ERROR', 'DEFAULT', 'Failed to change password. Please verify your current password.');
|
||||||
|
}
|
||||||
|
header("Location: $app_root?page=credentials");
|
||||||
|
exit();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// no form submitted, show the templates
|
||||||
|
} else {
|
||||||
|
// Get user timezone for templates
|
||||||
|
$userTimezone = !empty($userDetails[0]['timezone']) ? $userDetails[0]['timezone'] : 'UTC';
|
||||||
|
|
||||||
|
// Generate CSRF token if not exists
|
||||||
|
require_once '../app/helpers/security.php';
|
||||||
|
$security = SecurityHelper::getInstance();
|
||||||
|
$security->generateCsrfToken();
|
||||||
|
|
||||||
|
// Get 2FA status for the template
|
||||||
|
$has2fa = $userObject->isTwoFactorEnabled($user_id);
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'edit':
|
||||||
|
if (!$has2fa) {
|
||||||
|
require_once '../app/helpers/2fa.php';
|
||||||
|
$secret = $userObject->generateTwoFactorSecret();
|
||||||
|
$qrCode = $userObject->generateTwoFactorQR($user_id, $secret);
|
||||||
|
$backupCodes = $userObject->generateBackupCodes();
|
||||||
|
}
|
||||||
|
// Get any new feedback messages
|
||||||
|
include '../app/helpers/feedback.php';
|
||||||
|
|
||||||
|
// Load the 2FA setup template
|
||||||
|
include '../app/templates/credentials-2fa-setup.php';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'verify':
|
||||||
|
// Get any new feedback messages
|
||||||
|
include '../app/helpers/feedback.php';
|
||||||
|
|
||||||
|
// Load the 2FA verification template
|
||||||
|
include '../app/templates/credentials-2fa-verify.php';
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Get any new feedback messages
|
||||||
|
include '../app/helpers/feedback.php';
|
||||||
|
|
||||||
|
// Load the combined management template
|
||||||
|
include '../app/templates/credentials-manage.php';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Two-factor authentication setup template
|
||||||
|
*/
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Set up two-factor authentication</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<p>Two-factor authentication adds an extra layer of security to your account. Once enabled, you'll need to enter both your password and a code from your authenticator app when signing in.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (isset($error)): ?>
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<?php echo htmlspecialchars($error); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($setupData)): ?>
|
||||||
|
<div class="setup-steps">
|
||||||
|
<h4>1. Install an authenticator app</h4>
|
||||||
|
<p>If you haven't already, install an authenticator app on your mobile device:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Google Authenticator</li>
|
||||||
|
<li>Microsoft Authenticator</li>
|
||||||
|
<li>Authy</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4 class="mt-4">2. Scan the QR code</h4>
|
||||||
|
<p>Open your authenticator app and scan this QR code:</p>
|
||||||
|
|
||||||
|
<div class="text-center my-4">
|
||||||
|
<div id="qrcode"></div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<small class="text-muted">Can't scan? Use this code instead:</small><br>
|
||||||
|
<code class="secret-key"><?php echo htmlspecialchars($setupData['secret']); ?></code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 class="mt-4">3. Verify setup</h4>
|
||||||
|
<p>Enter the 6-digit code from your authenticator app to verify the setup:</p>
|
||||||
|
|
||||||
|
<form method="post" action="?page=credentials&action=setup" class="mt-3">
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="text"
|
||||||
|
name="code"
|
||||||
|
class="form-control"
|
||||||
|
pattern="[0-9]{6}"
|
||||||
|
maxlength="6"
|
||||||
|
required
|
||||||
|
placeholder="Enter 6-digit code">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="secret" value="<?php echo htmlspecialchars($setupData['secret']); ?>">
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary mt-3">
|
||||||
|
Verify and enable 2FA
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<h4>Backup codes</h4>
|
||||||
|
<p class="text-warning">
|
||||||
|
<strong>Important:</strong> Save these backup codes in a secure place.
|
||||||
|
If you lose access to your authenticator app, you can use these codes to sign in.
|
||||||
|
Each code can only be used once.
|
||||||
|
</p>
|
||||||
|
<div class="backup-codes bg-light p-3 rounded">
|
||||||
|
<?php foreach ($setupData['backupCodes'] as $code): ?>
|
||||||
|
<code class="d-block"><?php echo htmlspecialchars($code); ?></code>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-secondary mt-2" onclick="window.print()">
|
||||||
|
Print backup codes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<form method="post" action="?page=credentials&action=setup" class="mt-3">
|
||||||
|
<p>Click the button below to begin setting up two-factor authentication:</p>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
Begin setup
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (!empty($setupData)): ?>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
new QRCode(document.getElementById("qrcode"), {
|
||||||
|
text: <?php echo json_encode($setupData['otpauthUrl']); ?>,
|
||||||
|
width: 200,
|
||||||
|
height: 200
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php endif; ?>
|
|
@ -0,0 +1,86 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Two-factor authentication verification template
|
||||||
|
*/
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Two-factor authentication</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<?php if (isset($error)): ?>
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<?php echo htmlspecialchars($error); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<p>Enter the 6-digit code from your authenticator app:</p>
|
||||||
|
|
||||||
|
<form method="post" action="?page=credentials&action=verify" class="mt-3">
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="text"
|
||||||
|
name="code"
|
||||||
|
class="form-control form-control-lg text-center"
|
||||||
|
pattern="[0-9]{6}"
|
||||||
|
maxlength="6"
|
||||||
|
inputmode="numeric"
|
||||||
|
autocomplete="one-time-code"
|
||||||
|
required
|
||||||
|
autofocus
|
||||||
|
placeholder="000000">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="user_id" value="<?php echo htmlspecialchars($userId); ?>">
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary btn-block mt-4">
|
||||||
|
Verify code
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<p class="text-muted text-center">
|
||||||
|
Lost access to your authenticator app?<br>
|
||||||
|
<a href="#" data-toggle="collapse" data-target="#backupCodeForm">
|
||||||
|
Use a backup code
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="collapse mt-3" id="backupCodeForm">
|
||||||
|
<form method="post" action="?page=credentials&action=verify" class="mt-3">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Enter backup code:</label>
|
||||||
|
<input type="text"
|
||||||
|
name="backup_code"
|
||||||
|
class="form-control"
|
||||||
|
pattern="[a-f0-9]{8}"
|
||||||
|
maxlength="8"
|
||||||
|
required
|
||||||
|
placeholder="Enter backup code">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="user_id" value="<?php echo htmlspecialchars($userId); ?>">
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-secondary btn-block">
|
||||||
|
Use backup code
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Auto-submit when 6 digits are entered
|
||||||
|
document.querySelector('input[name="code"]').addEventListener('input', function(e) {
|
||||||
|
if (e.target.value.length === 6 && e.target.checkValidity()) {
|
||||||
|
e.target.form.submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,100 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Combined credentials management template
|
||||||
|
* Handles both password changes and 2FA management
|
||||||
|
*/
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<!-- Password Management -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>change password</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" action="?page=credentials&item=password">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="current_password">current password</label>
|
||||||
|
<input type="password"
|
||||||
|
class="form-control"
|
||||||
|
id="current_password"
|
||||||
|
name="current_password"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group mt-3">
|
||||||
|
<label for="new_password">new password</label>
|
||||||
|
<input type="password"
|
||||||
|
class="form-control"
|
||||||
|
id="new_password"
|
||||||
|
name="new_password"
|
||||||
|
pattern=".{8,}"
|
||||||
|
title="Password must be at least 8 characters long"
|
||||||
|
required>
|
||||||
|
<small class="form-text text-muted">minimum 8 characters</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group mt-3">
|
||||||
|
<label for="confirm_password">confirm new password</label>
|
||||||
|
<input type="password"
|
||||||
|
class="form-control"
|
||||||
|
id="confirm_password"
|
||||||
|
name="confirm_password"
|
||||||
|
pattern=".{8,}"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<button type="submit" class="btn btn-primary">change password</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2FA Management -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>two-factor authentication</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="mb-4">Two-factor authentication adds an extra layer of security to your account. Once enabled, you'll need to enter both your password and a code from your authenticator app when signing in.</p>
|
||||||
|
|
||||||
|
<?php if ($has2fa): ?>
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<i class="fas fa-check-circle"></i> two-factor authentication is enabled
|
||||||
|
</div>
|
||||||
|
<form method="post" action="?page=credentials&item=2fa&action=disable">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
|
||||||
|
<button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure you want to disable two-factor authentication? This will make your account less secure.')">
|
||||||
|
disable two-factor authentication
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i> two-factor authentication is not enabled
|
||||||
|
</div>
|
||||||
|
<form method="post" action="?page=credentials&item=2fa&action=edit">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
set up two-factor authentication
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('confirm_password').addEventListener('input', function() {
|
||||||
|
if (this.value !== document.getElementById('new_password').value) {
|
||||||
|
this.setCustomValidity('Passwords do not match');
|
||||||
|
} else {
|
||||||
|
this.setCustomValidity('');
|
||||||
|
}
|
||||||
|
});</script>
|
|
@ -0,0 +1,51 @@
|
||||||
|
|
||||||
|
<!-- Two-Factor Authentication -->
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Two-factor authentication</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<?php if ($has2fa): ?>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<p class="mb-0">
|
||||||
|
<i class="fas fa-shield-alt text-success"></i>
|
||||||
|
Two-factor authentication is enabled
|
||||||
|
</p>
|
||||||
|
<small class="text-muted">
|
||||||
|
Your account is protected with an authenticator app
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<form method="post" class="ml-3">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
|
||||||
|
<input type="hidden" name="item" value="2fa">
|
||||||
|
<input type="hidden" name="action" value="disable">
|
||||||
|
<button type="submit" class="btn btn-outline-danger"
|
||||||
|
onclick="return confirm('Are you sure you want to disable two-factor authentication? This will make your account less secure.')">
|
||||||
|
Disable 2FA
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<p class="mb-0">
|
||||||
|
<i class="fas fa-shield-alt text-muted"></i>
|
||||||
|
Two-factor authentication is not enabled
|
||||||
|
</p>
|
||||||
|
<small class="text-muted">
|
||||||
|
Add an extra layer of security to your account by requiring both your password and an authentication code
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<form method="post" class="ml-3">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
|
||||||
|
<input type="hidden" name="item" value="2fa">
|
||||||
|
<input type="hidden" name="action" value="enable">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
Enable 2FA
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -35,8 +35,24 @@
|
||||||
|
|
||||||
<ul class="menu-right">
|
<ul class="menu-right">
|
||||||
<?php if ( isset($_SESSION['username']) ) { ?>
|
<?php if ( isset($_SESSION['username']) ) { ?>
|
||||||
<li><a href="<?= htmlspecialchars($app_root) ?>?page=profile"><?= htmlspecialchars($currentUser) ?></a></li>
|
<li class="dropdown">
|
||||||
<li><a href="<?= htmlspecialchars($app_root) ?>?page=logout">logout</a></li>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
|
||||||
|
<i class="fas fa-user"></i>
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu dropdown-menu-right">
|
||||||
|
<h6 class="dropdown-header"><?= htmlspecialchars($currentUser) ?></h6>
|
||||||
|
<a class="dropdown-item" href="<?= htmlspecialchars($app_root) ?>?page=profile">
|
||||||
|
<i class="fas fa-id-card"></i>Profile details
|
||||||
|
</a>
|
||||||
|
<a class="dropdown-item" href="<?= htmlspecialchars($app_root) ?>?page=credentials">
|
||||||
|
<i class="fas fa-shield-alt"></i>Login credentials
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<a class="dropdown-item" href="<?= htmlspecialchars($app_root) ?>?page=logout">
|
||||||
|
<i class="fas fa-sign-out-alt"></i>Logout
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
<?php } else { ?>
|
<?php } else { ?>
|
||||||
<li><a href="<?= htmlspecialchars($app_root) ?>?page=login">login</a></li>
|
<li><a href="<?= htmlspecialchars($app_root) ?>?page=login">login</a></li>
|
||||||
<li><a href="<?= htmlspecialchars($app_root) ?>?page=register">register</a></li>
|
<li><a href="<?= htmlspecialchars($app_root) ?>?page=register">register</a></li>
|
||||||
|
|
|
@ -53,6 +53,7 @@ $allowed_urls = [
|
||||||
'config',
|
'config',
|
||||||
|
|
||||||
'profile',
|
'profile',
|
||||||
|
'credentials',
|
||||||
|
|
||||||
'settings',
|
'settings',
|
||||||
'security',
|
'security',
|
||||||
|
|
|
@ -44,6 +44,57 @@ html, body {
|
||||||
.menu-left li a:hover, .menu-right li a:hover {
|
.menu-left li a:hover, .menu-right li a:hover {
|
||||||
background-color: #111;
|
background-color: #111;
|
||||||
}
|
}
|
||||||
|
/* Dropdown menu styles */
|
||||||
|
.dropdown-menu {
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid rgba(0,0,0,.15);
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,.175);
|
||||||
|
margin-top: 0;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu .dropdown-item {
|
||||||
|
color: #333;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu .dropdown-item:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #16181b;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu .dropdown-item i {
|
||||||
|
width: 16px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu .dropdown-item:hover i {
|
||||||
|
color: #16181b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-divider {
|
||||||
|
margin: 4px 0;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-toggle::after {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
vertical-align: middle;
|
||||||
|
content: "";
|
||||||
|
border-top: 4px solid;
|
||||||
|
border-right: 4px solid transparent;
|
||||||
|
border-left: 4px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
/* /menu */
|
/* /menu */
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
|
|
Loading…
Reference in New Issue