Compare commits
3 Commits
71b0448004
...
f27f3fe62f
Author | SHA1 | Date |
---|---|---|
|
f27f3fe62f | |
|
0d4251b321 | |
|
11fa58bd6e |
|
@ -0,0 +1,172 @@
|
||||||
|
<?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,6 +142,8 @@ 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'],
|
||||||
|
@ -151,10 +153,7 @@ 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);
|
||||||
return [
|
throw new Exception("Invalid credentials. {$remainingAttempts} attempts remaining.");
|
||||||
'status' => 'failed',
|
|
||||||
'message' => "Invalid credentials. {$remainingAttempts} attempts remaining."
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,8 @@ 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
|
||||||
$dbWeb = connectDB($config)['db'];
|
$db = connectDB($config)['db'];
|
||||||
|
|
||||||
// Initialize RateLimiter
|
// Initialize RateLimiter
|
||||||
require_once '../app/classes/ratelimiter.php';
|
require_once '../app/classes/ratelimiter.php';
|
||||||
$rateLimiter = new RateLimiter($dbWeb);
|
$rateLimiter = new RateLimiter($db);
|
||||||
|
|
||||||
// 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($dbWeb);
|
$twoFactorAuth = new TwoFactorAuthentication($db);
|
||||||
|
|
||||||
if ($twoFactorAuth->verify($userId, $code)) {
|
if ($twoFactorAuth->verify($userId, $code)) {
|
||||||
// Complete login
|
// Complete login
|
||||||
|
@ -63,6 +63,122 @@ 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,17 +2,17 @@
|
||||||
<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 JILO!</strong><br />Please enter login credentials:</p>
|
<p class="card-text"><strong>Welcome to <?= htmlspecialchars($config['site_name']); ?>!</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">
|
||||||
<input type="text" class="form-control w-50 mx-auto" name="username" placeholder="Username"
|
<input type="text" class="form-control w-50 mx-auto" name="username" placeholder="Username"
|
||||||
pattern="[A-Za-z0-9_\-]{3,20}" title="3-20 characters, letters, numbers, - and _"
|
pattern="[A-Za-z0-9_\-]{3,20}" title="3-20 characters, letters, numbers, - and _"
|
||||||
required autofocus />
|
required autofocus />
|
||||||
</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=".{2,}" title="Eight or more characters"
|
pattern=".{5,}" title="Eight or more characters"
|
||||||
required />
|
required />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group mb-3">
|
<div class="form-group mb-3">
|
||||||
|
@ -23,6 +23,9 @@
|
||||||
</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 -->
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
|
||||||
|
<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>
|
|
@ -0,0 +1,38 @@
|
||||||
|
|
||||||
|
<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,6 +63,15 @@ 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,6 +24,14 @@ 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
|
||||||
|
@ -33,78 +41,65 @@ 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 = [
|
$_SESSION['LAST_ACTIVITY'] = time() - 1500; // 25 minutes ago
|
||||||
'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 = [
|
$_SESSION['CREATED'] = $now - 1900; // 31+ minutes ago
|
||||||
'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 = [
|
$_SESSION['REMEMBER_ME'] = true;
|
||||||
'USER_ID' => 1,
|
$_SESSION['LAST_ACTIVITY'] = time() - 86500; // More than 24 hours ago
|
||||||
'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()
|
||||||
{
|
{
|
||||||
$_SESSION = [];
|
unset($_SESSION['user_id']);
|
||||||
$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 = [
|
$_SESSION['LAST_ACTIVITY'] = time() - 1500; // 25 minutes ago
|
||||||
'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,6 +44,19 @@ 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 (
|
||||||
|
@ -116,7 +129,13 @@ 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->assertTrue($result);
|
$this->assertIsArray($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