Compare commits
No commits in common. "f27f3fe62fc92e62331176de88ef0d07e47526e8" and "71b0448004094c5ed4c8d0058a944ec824588ede" have entirely different histories.
f27f3fe62f
...
71b0448004
|
@ -1,172 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles password reset functionality including token generation and validation
|
|
||||||
*/
|
|
||||||
class PasswordReset {
|
|
||||||
private $db;
|
|
||||||
private const TOKEN_LENGTH = 32;
|
|
||||||
private const TOKEN_EXPIRY = 3600; // 1 hour
|
|
||||||
|
|
||||||
public function __construct($database) {
|
|
||||||
if ($database instanceof PDO) {
|
|
||||||
$this->db = $database;
|
|
||||||
} else {
|
|
||||||
$this->db = $database->getConnection();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a password reset request and sends email to user
|
|
||||||
*
|
|
||||||
* @param string $email User's email address
|
|
||||||
* @return array Status of the reset request
|
|
||||||
*/
|
|
||||||
public function requestReset($email) {
|
|
||||||
// Check if email exists
|
|
||||||
$query = $this->db->prepare("
|
|
||||||
SELECT u.id, um.email
|
|
||||||
FROM users u
|
|
||||||
JOIN users_meta um ON u.id = um.user_id
|
|
||||||
WHERE um.email = :email"
|
|
||||||
);
|
|
||||||
$query->bindParam(':email', $email);
|
|
||||||
$query->execute();
|
|
||||||
|
|
||||||
$user = $query->fetch(PDO::FETCH_ASSOC);
|
|
||||||
if (!$user) {
|
|
||||||
return ['success' => false, 'message' => 'If this email exists in our system, you will receive reset instructions.'];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate unique token
|
|
||||||
$token = bin2hex(random_bytes(self::TOKEN_LENGTH / 2));
|
|
||||||
$expires = time() + self::TOKEN_EXPIRY;
|
|
||||||
|
|
||||||
// Store token in database
|
|
||||||
$query = $this->db->prepare("
|
|
||||||
INSERT INTO user_password_reset (user_id, token, expires)
|
|
||||||
VALUES (:user_id, :token, :expires)"
|
|
||||||
);
|
|
||||||
$query->bindParam(':user_id', $user['id']);
|
|
||||||
$query->bindParam(':token', $token);
|
|
||||||
$query->bindParam(':expires', $expires);
|
|
||||||
|
|
||||||
if (!$query->execute()) {
|
|
||||||
return ['success' => false, 'message' => 'Failed to process reset request'];
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need the config for the email details
|
|
||||||
global $config;
|
|
||||||
|
|
||||||
// Prepare the reset link
|
|
||||||
$scheme = $_SERVER['REQUEST_SCHEME'];
|
|
||||||
$domain = trim($config['domain'], '/');
|
|
||||||
$folder = trim($config['folder'], '/');
|
|
||||||
$folderPath = $folder !== '' ? "/$folder" : '';
|
|
||||||
$resetLink = "{$scheme}://{$domain}{$folderPath}/index.php?page=login&action=reset&token=" . urlencode($token);
|
|
||||||
|
|
||||||
// Send email with reset link
|
|
||||||
$to = $user['email'];
|
|
||||||
$subject = "{$config['site_name']} - Password reset request";
|
|
||||||
$message = "Dear user,\n\n";
|
|
||||||
$message .= "We received a request to reset your password for your {$config['site_name']} account.\n\n";
|
|
||||||
$message .= "To set a new password, please click the link below:\n\n";
|
|
||||||
$message .= $resetLink . "\n\n";
|
|
||||||
$message .= "This link will expire in 1 hour for security reasons.\n\n";
|
|
||||||
$message .= "If you did not request this password reset, please ignore this email. Your account remains secure.\n\n";
|
|
||||||
if (!empty($config['site_name'])) {
|
|
||||||
$message .= "Best regards,\n";
|
|
||||||
$message .= "The {$config['site_name']} team\n";
|
|
||||||
if (!empty($config['site_slogan'])) {
|
|
||||||
$message .= ":: {$config['site_slogan']} ::";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$headers = [
|
|
||||||
'From' => "noreply@{$config['domain']}",
|
|
||||||
'Reply-To' => "noreply@{$config['domain']}",
|
|
||||||
'X-Mailer' => 'PHP/' . phpversion()
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!mail($to, $subject, $message, $headers)) {
|
|
||||||
return ['success' => false, 'message' => 'Failed to send reset email'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['success' => true, 'message' => 'If this email exists in our system, you will receive reset instructions.'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a reset token and returns associated user ID if valid
|
|
||||||
*
|
|
||||||
* @param string $token Reset token
|
|
||||||
* @return array Validation result with user ID if successful
|
|
||||||
*/
|
|
||||||
public function validateToken($token) {
|
|
||||||
$now = time();
|
|
||||||
|
|
||||||
$query = $this->db->prepare("
|
|
||||||
SELECT user_id
|
|
||||||
FROM user_password_reset
|
|
||||||
WHERE token = :token
|
|
||||||
AND expires > :now
|
|
||||||
AND used = 0
|
|
||||||
");
|
|
||||||
|
|
||||||
$query->bindParam(':token', $token);
|
|
||||||
$query->bindParam(':now', $now);
|
|
||||||
$query->execute();
|
|
||||||
|
|
||||||
$result = $query->fetch(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
if (!$result) {
|
|
||||||
return ['valid' => false];
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['valid' => true, 'user_id' => $result['user_id']];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Completes the password reset process
|
|
||||||
*
|
|
||||||
* @param string $token Reset token
|
|
||||||
* @param string $newPassword New password
|
|
||||||
* @return bool Whether reset was successful
|
|
||||||
*/
|
|
||||||
public function resetPassword($token, $newPassword) {
|
|
||||||
$validation = $this->validateToken($token);
|
|
||||||
if (!$validation['valid']) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start transaction
|
|
||||||
$this->db->beginTransaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Update password
|
|
||||||
$hashedPassword = password_hash($newPassword, PASSWORD_DEFAULT);
|
|
||||||
$query = $this->db->prepare(
|
|
||||||
"UPDATE user
|
|
||||||
SET password = :password
|
|
||||||
WHERE id = :user_id"
|
|
||||||
);
|
|
||||||
$query->bindParam(':password', $hashedPassword);
|
|
||||||
$query->bindParam(':user_id', $validation['user_id']);
|
|
||||||
$query->execute();
|
|
||||||
|
|
||||||
// Mark token as used
|
|
||||||
$query = $this->db->prepare(
|
|
||||||
"UPDATE user_password_reset
|
|
||||||
SET used = 1
|
|
||||||
WHERE token = :token"
|
|
||||||
);
|
|
||||||
$query->bindParam(':token', $token);
|
|
||||||
$query->execute();
|
|
||||||
|
|
||||||
$this->db->commit();
|
|
||||||
return true;
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$this->db->rollBack();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -142,8 +142,6 @@ class User {
|
||||||
// Login successful
|
// Login successful
|
||||||
$_SESSION['user_id'] = $user['id'];
|
$_SESSION['user_id'] = $user['id'];
|
||||||
$_SESSION['username'] = $user['username'];
|
$_SESSION['username'] = $user['username'];
|
||||||
$_SESSION['CREATED'] = time();
|
|
||||||
$_SESSION['LAST_ACTIVITY'] = time();
|
|
||||||
return [
|
return [
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'user_id' => $user['id'],
|
'user_id' => $user['id'],
|
||||||
|
@ -153,7 +151,10 @@ class User {
|
||||||
|
|
||||||
// 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."
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -10,8 +10,6 @@ return [
|
||||||
'domain' => 'localhost',
|
'domain' => 'localhost',
|
||||||
// subfolder for the web app, if any
|
// subfolder for the web app, if any
|
||||||
'folder' => '/jilo-web/',
|
'folder' => '/jilo-web/',
|
||||||
// site name used in emails and in the inteerface
|
|
||||||
'site_name' => 'Jilo Web',
|
|
||||||
// set to false to disable new registrations
|
// set to false to disable new registrations
|
||||||
'registration_enabled' => '1',
|
'registration_enabled' => '1',
|
||||||
// will be displayed on login screen
|
// will be displayed on login screen
|
||||||
|
|
|
@ -19,11 +19,11 @@ unset($error);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// connect to database
|
// connect to database
|
||||||
$db = connectDB($config)['db'];
|
$dbWeb = connectDB($config)['db'];
|
||||||
|
|
||||||
// Initialize RateLimiter
|
// Initialize RateLimiter
|
||||||
require_once '../app/classes/ratelimiter.php';
|
require_once '../app/classes/ratelimiter.php';
|
||||||
$rateLimiter = new RateLimiter($db);
|
$rateLimiter = new RateLimiter($dbWeb);
|
||||||
|
|
||||||
// Get user IP
|
// Get user IP
|
||||||
$user_IP = getUserIP();
|
$user_IP = getUserIP();
|
||||||
|
@ -38,7 +38,7 @@ try {
|
||||||
$rememberMe = isset($_SESSION['2fa_pending_remember']);
|
$rememberMe = isset($_SESSION['2fa_pending_remember']);
|
||||||
|
|
||||||
require_once '../app/classes/twoFactorAuth.php';
|
require_once '../app/classes/twoFactorAuth.php';
|
||||||
$twoFactorAuth = new TwoFactorAuthentication($db);
|
$twoFactorAuth = new TwoFactorAuthentication($dbWeb);
|
||||||
|
|
||||||
if ($twoFactorAuth->verify($userId, $code)) {
|
if ($twoFactorAuth->verify($userId, $code)) {
|
||||||
// Complete login
|
// Complete login
|
||||||
|
@ -63,122 +63,6 @@ try {
|
||||||
// Load the 2FA verification template
|
// Load the 2FA verification template
|
||||||
include '../app/templates/credentials-2fa-verify.php';
|
include '../app/templates/credentials-2fa-verify.php';
|
||||||
exit();
|
exit();
|
||||||
} elseif ($action === 'forgot') {
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
// Handle password reset request
|
|
||||||
try {
|
|
||||||
// Validate CSRF token
|
|
||||||
$security = SecurityHelper::getInstance();
|
|
||||||
if (!$security->verifyCsrfToken($_POST['csrf_token'] ?? '')) {
|
|
||||||
throw new Exception('Invalid security token. Please try again.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply rate limiting
|
|
||||||
if (!$rateLimiter->isIpWhitelisted($user_IP)) {
|
|
||||||
if ($rateLimiter->isIpBlacklisted($user_IP)) {
|
|
||||||
throw new Exception(Feedback::get('LOGIN', 'IP_BLACKLISTED')['message']);
|
|
||||||
}
|
|
||||||
if ($rateLimiter->tooManyAttempts('password_reset', $user_IP)) {
|
|
||||||
throw new Exception(Feedback::get('LOGIN', 'TOO_MANY_ATTEMPTS')['message']);
|
|
||||||
}
|
|
||||||
$rateLimiter->attempt('password_reset', $user_IP);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate email
|
|
||||||
$email = filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL);
|
|
||||||
if (!$email) {
|
|
||||||
throw new Exception('Please enter a valid email address.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process reset request
|
|
||||||
require_once '../app/classes/passwordReset.php';
|
|
||||||
$resetHandler = new PasswordReset($db);
|
|
||||||
$result = $resetHandler->requestReset($email);
|
|
||||||
|
|
||||||
// Always show same message whether email exists or not for security
|
|
||||||
Feedback::flash('NOTICE', 'DEFAULT', $result['message']);
|
|
||||||
header("Location: $app_root?page=login");
|
|
||||||
exit();
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
Feedback::flash('ERROR', 'DEFAULT', $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate CSRF token
|
|
||||||
$security = SecurityHelper::getInstance();
|
|
||||||
$security->generateCsrfToken();
|
|
||||||
|
|
||||||
// Load the forgot password form
|
|
||||||
include '../app/helpers/feedback.php';
|
|
||||||
include '../app/templates/form-password-forgot.php';
|
|
||||||
exit();
|
|
||||||
|
|
||||||
} elseif ($action === 'reset' && isset($_GET['token'])) {
|
|
||||||
// Handle password reset
|
|
||||||
try {
|
|
||||||
require_once '../app/classes/passwordReset.php';
|
|
||||||
$resetHandler = new PasswordReset($db);
|
|
||||||
$token = $_GET['token'];
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
// Validate CSRF token
|
|
||||||
$security = SecurityHelper::getInstance();
|
|
||||||
if (!$security->verifyCsrfToken($_POST['csrf_token'] ?? '')) {
|
|
||||||
throw new Exception('Invalid security token. Please try again.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply rate limiting
|
|
||||||
if (!$rateLimiter->isIpWhitelisted($user_IP)) {
|
|
||||||
if ($rateLimiter->tooManyAttempts('password_reset', $user_IP)) {
|
|
||||||
throw new Exception(Feedback::get('LOGIN', 'TOO_MANY_ATTEMPTS')['message']);
|
|
||||||
}
|
|
||||||
$rateLimiter->attempt('password_reset', $user_IP);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate password
|
|
||||||
require_once '../app/classes/validator.php';
|
|
||||||
$validator = new Validator($_POST);
|
|
||||||
$rules = [
|
|
||||||
'new_password' => [
|
|
||||||
'required' => true,
|
|
||||||
'min' => 8
|
|
||||||
],
|
|
||||||
'confirm_password' => [
|
|
||||||
'required' => true,
|
|
||||||
'matches' => 'new_password'
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!$validator->validate($rules)) {
|
|
||||||
throw new Exception($validator->getFirstError());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset password
|
|
||||||
if ($resetHandler->resetPassword($token, $_POST['new_password'])) {
|
|
||||||
Feedback::flash('NOTICE', 'DEFAULT', 'Your password has been reset successfully. You can now log in.');
|
|
||||||
header("Location: $app_root?page=login");
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
throw new Exception('Invalid or expired reset link. Please request a new one.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify token is valid
|
|
||||||
$validation = $resetHandler->validateToken($token);
|
|
||||||
if (!$validation['valid']) {
|
|
||||||
throw new Exception('Invalid or expired reset link. Please request a new one.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show reset password form
|
|
||||||
include '../app/helpers/feedback.php';
|
|
||||||
include '../app/templates/form-password-reset.php';
|
|
||||||
exit();
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
Feedback::flash('ERROR', 'DEFAULT', $e->getMessage());
|
|
||||||
header("Location: $app_root?page=login&action=forgot");
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( $_SERVER['REQUEST_METHOD'] == 'POST' && $action !== 'verify' ) {
|
if ( $_SERVER['REQUEST_METHOD'] == 'POST' && $action !== 'verify' ) {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="card text-center w-50 mx-auto">
|
<div class="card text-center w-50 mx-auto">
|
||||||
<h2 class="card-header">Login</h2>
|
<h2 class="card-header">Login</h2>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="card-text"><strong>Welcome to <?= htmlspecialchars($config['site_name']); ?>!</strong><br />Please enter login credentials:</p>
|
<p class="card-text"><strong>Welcome to JILO!</strong><br />Please enter login credentials:</p>
|
||||||
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=login">
|
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=login">
|
||||||
<?php include 'csrf_token.php'; ?>
|
<?php include 'csrf_token.php'; ?>
|
||||||
<div class="form-group mb-3">
|
<div class="form-group mb-3">
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group mb-3">
|
<div class="form-group mb-3">
|
||||||
<input type="password" class="form-control w-50 mx-auto" name="password" placeholder="Password"
|
<input type="password" class="form-control w-50 mx-auto" name="password" placeholder="Password"
|
||||||
pattern=".{5,}" title="Eight or more characters"
|
pattern=".{2,}" title="Eight or more characters"
|
||||||
required />
|
required />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group mb-3">
|
<div class="form-group mb-3">
|
||||||
|
@ -23,9 +23,6 @@
|
||||||
</div>
|
</div>
|
||||||
<input type="submit" class="btn btn-primary" value="Login" />
|
<input type="submit" class="btn btn-primary" value="Login" />
|
||||||
</form>
|
</form>
|
||||||
<div class="mt-3">
|
|
||||||
<a href="?page=login&action=forgot">forgot password?</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- /login form -->
|
<!-- /login form -->
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<div class="row justify-content-center">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="card mt-5">
|
|
||||||
<div class="card-body">
|
|
||||||
<h3 class="card-title mb-4">Reset password</h3>
|
|
||||||
<p>Enter your email address and we will send you<br />
|
|
||||||
instructions to reset your password.</p>
|
|
||||||
<form method="post" action="?page=login&action=forgot">
|
|
||||||
<?php include 'csrf_token.php'; ?>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="email">email address:</label>
|
|
||||||
<input type="email"
|
|
||||||
class="form-control"
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
required
|
|
||||||
autocomplete="email">
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary btn-block mt-4">
|
|
||||||
Send reset instructions
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<div class="mt-3 text-center">
|
|
||||||
<a href="?page=login">back to login</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,38 +0,0 @@
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<div class="row justify-content-center">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="card mt-5">
|
|
||||||
<div class="card-body">
|
|
||||||
<h3 class="card-title mb-4">Set new password</h3>
|
|
||||||
<form method="post" action="?page=login&action=reset&token=<?= htmlspecialchars(urlencode($token)) ?>">
|
|
||||||
<?php include 'csrf_token.php'; ?>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="new_password">new password:</label>
|
|
||||||
<input type="password"
|
|
||||||
class="form-control"
|
|
||||||
id="new_password"
|
|
||||||
name="new_password"
|
|
||||||
required
|
|
||||||
minlength="8"
|
|
||||||
autocomplete="new-password">
|
|
||||||
</div>
|
|
||||||
<div class="form-group mt-3">
|
|
||||||
<label for="confirm_password">confirm password:</label>
|
|
||||||
<input type="password"
|
|
||||||
class="form-control"
|
|
||||||
id="confirm_password"
|
|
||||||
name="confirm_password"
|
|
||||||
required
|
|
||||||
minlength="8"
|
|
||||||
autocomplete="new-password">
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary btn-block mt-4">
|
|
||||||
Set new password
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -63,15 +63,6 @@ CREATE TABLE user_2fa_temp (
|
||||||
PRIMARY KEY (user_id, code),
|
PRIMARY KEY (user_id, code),
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
);
|
);
|
||||||
CREATE TABLE user_password_reset (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
token TEXT NOT NULL UNIQUE,
|
|
||||||
expires INTEGER NOT NULL,
|
|
||||||
used INTEGER NOT NULL DEFAULT 0,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
CREATE TABLE logs (
|
CREATE TABLE logs (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
user_id INTEGER NOT NULL,
|
user_id INTEGER NOT NULL,
|
||||||
|
|
|
@ -24,14 +24,6 @@ class SessionMiddlewareTest extends TestCase
|
||||||
'domain' => 'localhost'
|
'domain' => 'localhost'
|
||||||
];
|
];
|
||||||
$this->app_root = 'https://localhost/app';
|
$this->app_root = 'https://localhost/app';
|
||||||
|
|
||||||
// Initialize session variables
|
|
||||||
$_SESSION = [
|
|
||||||
'user_id' => 1,
|
|
||||||
'username' => 'testuser',
|
|
||||||
'CREATED' => time(),
|
|
||||||
'LAST_ACTIVITY' => time()
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function tearDown(): void
|
protected function tearDown(): void
|
||||||
|
@ -41,65 +33,78 @@ class SessionMiddlewareTest extends TestCase
|
||||||
|
|
||||||
public function testSessionStart()
|
public function testSessionStart()
|
||||||
{
|
{
|
||||||
|
$_SESSION = ['USER_ID' => 1];
|
||||||
$result = applySessionMiddleware($this->config, $this->app_root);
|
$result = applySessionMiddleware($this->config, $this->app_root);
|
||||||
|
|
||||||
$this->assertTrue($result);
|
$this->assertTrue($result);
|
||||||
$this->assertArrayHasKey('LAST_ACTIVITY', $_SESSION);
|
$this->assertArrayHasKey('LAST_ACTIVITY', $_SESSION);
|
||||||
$this->assertArrayHasKey('CREATED', $_SESSION);
|
$this->assertArrayHasKey('CREATED', $_SESSION);
|
||||||
$this->assertArrayHasKey('user_id', $_SESSION);
|
$this->assertArrayHasKey('USER_ID', $_SESSION);
|
||||||
$this->assertEquals(1, $_SESSION['user_id']);
|
$this->assertEquals(1, $_SESSION['USER_ID']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSessionTimeout()
|
public function testSessionTimeout()
|
||||||
{
|
{
|
||||||
$_SESSION['LAST_ACTIVITY'] = time() - 1500; // 25 minutes ago
|
$_SESSION = [
|
||||||
|
'USER_ID' => 1,
|
||||||
|
'LAST_ACTIVITY' => time() - 1500 // 25 minutes ago
|
||||||
|
];
|
||||||
|
|
||||||
$result = applySessionMiddleware($this->config, $this->app_root);
|
$result = applySessionMiddleware($this->config, $this->app_root);
|
||||||
|
|
||||||
$this->assertFalse($result);
|
$this->assertFalse($result);
|
||||||
$this->assertArrayNotHasKey('user_id', $_SESSION, 'Session should be cleared after timeout');
|
$this->assertArrayNotHasKey('USER_ID', $_SESSION, 'Session should be cleared after timeout');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSessionRegeneration()
|
public function testSessionRegeneration()
|
||||||
{
|
{
|
||||||
$now = time();
|
$now = time();
|
||||||
$_SESSION['CREATED'] = $now - 1900; // 31+ minutes ago
|
$_SESSION = [
|
||||||
|
'USER_ID' => 1,
|
||||||
|
'CREATED' => $now - 1900 // 31+ minutes ago
|
||||||
|
];
|
||||||
|
|
||||||
$result = applySessionMiddleware($this->config, $this->app_root);
|
$result = applySessionMiddleware($this->config, $this->app_root);
|
||||||
|
|
||||||
$this->assertTrue($result);
|
$this->assertTrue($result);
|
||||||
$this->assertEquals(1, $_SESSION['user_id']);
|
$this->assertEquals(1, $_SESSION['USER_ID']);
|
||||||
$this->assertGreaterThanOrEqual($now - 1900, $_SESSION['CREATED']);
|
$this->assertGreaterThanOrEqual($now - 1900, $_SESSION['CREATED']);
|
||||||
$this->assertLessThanOrEqual($now + 10, $_SESSION['CREATED']);
|
$this->assertLessThanOrEqual($now + 10, $_SESSION['CREATED']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testRememberMe()
|
public function testRememberMe()
|
||||||
{
|
{
|
||||||
$_SESSION['REMEMBER_ME'] = true;
|
$_SESSION = [
|
||||||
$_SESSION['LAST_ACTIVITY'] = time() - 86500; // More than 24 hours ago
|
'USER_ID' => 1,
|
||||||
|
'REMEMBER_ME' => true,
|
||||||
|
'LAST_ACTIVITY' => time() - 86500 // More than 24 hours ago
|
||||||
|
];
|
||||||
|
|
||||||
$result = applySessionMiddleware($this->config, $this->app_root);
|
$result = applySessionMiddleware($this->config, $this->app_root);
|
||||||
|
|
||||||
$this->assertTrue($result);
|
$this->assertTrue($result);
|
||||||
$this->assertArrayHasKey('user_id', $_SESSION);
|
$this->assertArrayHasKey('USER_ID', $_SESSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testNoUserSession()
|
public function testNoUserSession()
|
||||||
{
|
{
|
||||||
unset($_SESSION['user_id']);
|
$_SESSION = [];
|
||||||
$result = applySessionMiddleware($this->config, $this->app_root);
|
$result = applySessionMiddleware($this->config, $this->app_root);
|
||||||
|
|
||||||
$this->assertFalse($result);
|
$this->assertFalse($result);
|
||||||
$this->assertArrayNotHasKey('user_id', $_SESSION);
|
$this->assertArrayNotHasKey('USER_ID', $_SESSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSessionHeaders()
|
public function testSessionHeaders()
|
||||||
{
|
{
|
||||||
$_SESSION['LAST_ACTIVITY'] = time() - 1500; // 25 minutes ago
|
$_SESSION = [
|
||||||
|
'USER_ID' => 1,
|
||||||
|
'LAST_ACTIVITY' => time() - 1500 // 25 minutes ago
|
||||||
|
];
|
||||||
|
|
||||||
$result = applySessionMiddleware($this->config, $this->app_root);
|
$result = applySessionMiddleware($this->config, $this->app_root);
|
||||||
|
|
||||||
$this->assertFalse($result);
|
$this->assertFalse($result);
|
||||||
$this->assertArrayNotHasKey('user_id', $_SESSION, 'Session should be cleared after timeout');
|
$this->assertArrayNotHasKey('USER_ID', $_SESSION, 'Session should be cleared after timeout');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,19 +44,6 @@ class UserTest extends TestCase
|
||||||
)
|
)
|
||||||
");
|
");
|
||||||
|
|
||||||
// Create user_2fa table for two-factor authentication
|
|
||||||
$this->db->getConnection()->exec("
|
|
||||||
CREATE TABLE user_2fa (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
secret_key TEXT NOT NULL,
|
|
||||||
backup_codes TEXT,
|
|
||||||
enabled TINYINT(1) NOT NULL DEFAULT 0,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
||||||
)
|
|
||||||
");
|
|
||||||
|
|
||||||
// Create tables for rate limiter
|
// Create tables for rate limiter
|
||||||
$this->db->getConnection()->exec("
|
$this->db->getConnection()->exec("
|
||||||
CREATE TABLE login_attempts (
|
CREATE TABLE login_attempts (
|
||||||
|
@ -129,13 +116,7 @@ class UserTest extends TestCase
|
||||||
// Test successful login
|
// Test successful login
|
||||||
try {
|
try {
|
||||||
$result = $this->user->login('testuser', $password);
|
$result = $this->user->login('testuser', $password);
|
||||||
$this->assertIsArray($result);
|
$this->assertTrue($result);
|
||||||
$this->assertEquals('success', $result['status']);
|
|
||||||
$this->assertArrayHasKey('user_id', $result);
|
|
||||||
$this->assertArrayHasKey('username', $result);
|
|
||||||
$this->assertArrayHasKey('user_id', $_SESSION);
|
|
||||||
$this->assertArrayHasKey('CREATED', $_SESSION);
|
|
||||||
$this->assertArrayHasKey('LAST_ACTIVITY', $_SESSION);
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$this->fail('Login should not throw an exception for valid credentials: ' . $e->getMessage());
|
$this->fail('Login should not throw an exception for valid credentials: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue