Compare commits

..

5 Commits

9 changed files with 332 additions and 202 deletions

View File

@ -8,11 +8,12 @@
*/
class TwoFactorAuthentication {
private $db;
private $secretLength = 32;
private $period = 30; // Time step in seconds
private $digits = 6; // Number of digits in TOTP code
private $algorithm = 'sha1';
private $issuer = 'Jilo';
private $secretLength = 20; // 160 bits for SHA1
private $period = 30; // Time step in seconds (T0)
private $digits = 6; // Number of digits in TOTP code
private $algorithm = 'sha1'; // HMAC algorithm
private $issuer = 'TotalMeet';
private $window = 1; // Time window of 1 step before/after
/**
* Constructor
@ -31,9 +32,11 @@ class TwoFactorAuthentication {
* Enable 2FA for a user
*
* @param int $userId User ID
* @return array Array containing success status and data (secret, QR code URL)
* @param string $secret Secret key (base32 encoded)
* @param string $code Verification code
* @return bool True if enabled successfully
*/
public function enable($userId) {
public function enable($userId, $secret = null, $code = null) {
try {
// Check if 2FA is already enabled
$stmt = $this->db->prepare('SELECT enabled FROM user_2fa WHERE user_id = ?');
@ -41,58 +44,81 @@ class TwoFactorAuthentication {
$existing = $stmt->fetch(PDO::FETCH_ASSOC);
if ($existing && $existing['enabled']) {
return ['success' => false, 'message' => '2FA is already enabled'];
return false;
}
// Generate secret key
$secret = $this->generateSecret();
// If no secret provided, generate one and return setup data
if ($secret === null) {
// Generate secret key
$secret = $this->generateSecret();
// Get user's username for the QR code
$stmt = $this->db->prepare('SELECT username FROM users WHERE id = ?');
$stmt->execute([$userId]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
// Get user's username for the QR code
$stmt = $this->db->prepare('SELECT username FROM user WHERE id = ?');
$stmt->execute([$userId]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
// Generate backup codes
$backupCodes = $this->generateBackupCodes();
// Generate backup codes
$backupCodes = $this->generateBackupCodes();
// Store in database
$this->db->beginTransaction();
// Store in database without enabling yet
$this->db->beginTransaction();
$stmt = $this->db->prepare('
INSERT INTO user_2fa (user_id, secret_key, backup_codes, enabled, created_at)
VALUES (?, ?, ?, 0, NOW())
ON DUPLICATE KEY UPDATE
secret_key = VALUES(secret_key),
backup_codes = VALUES(backup_codes),
enabled = VALUES(enabled),
created_at = VALUES(created_at)
');
$stmt = $this->db->prepare('
INSERT INTO user_2fa (user_id, secret_key, backup_codes, enabled, created_at)
VALUES (?, ?, ?, 0, NOW())
ON DUPLICATE KEY UPDATE
secret_key = VALUES(secret_key),
backup_codes = VALUES(backup_codes),
enabled = VALUES(enabled),
created_at = VALUES(created_at)
');
$stmt->execute([
$userId,
$secret,
json_encode($backupCodes)
]);
$stmt->execute([
$userId,
$secret,
json_encode($backupCodes)
]);
$this->db->commit();
$this->db->commit();
// Generate otpauth URL for QR code
$otpauthUrl = $this->generateOtpauthUrl($user['username'], $secret);
// Generate otpauth URL for QR code
$otpauthUrl = $this->generateOtpauthUrl($user['username'], $secret);
return [
'success' => true,
'data' => [
'secret' => $secret,
'otpauthUrl' => $otpauthUrl,
'backupCodes' => $backupCodes
]
];
return [
'success' => true,
'data' => [
'secret' => $secret,
'otpauthUrl' => $otpauthUrl,
'backupCodes' => $backupCodes
]
];
}
// If secret and code provided, verify the code and enable 2FA
if ($code !== null) {
// Verify the setup code
if (!$this->verify($userId, $code)) {
error_log("Code verification failed");
return false;
}
// Enable 2FA
$stmt = $this->db->prepare('
UPDATE user_2fa
SET enabled = 1
WHERE user_id = ? AND secret_key = ?
');
return $stmt->execute([$userId, $secret]);
}
return false;
} catch (Exception $e) {
if ($this->db->inTransaction()) {
$this->db->rollBack();
}
return ['success' => false, 'message' => $e->getMessage()];
error_log('2FA enable error: ' . $e->getMessage());
return false;
}
}
@ -106,37 +132,24 @@ class TwoFactorAuthentication {
public function verify($userId, $code) {
try {
// Get user's 2FA settings
$stmt = $this->db->prepare('
SELECT secret_key, backup_codes, enabled
FROM user_2fa
WHERE user_id = ?
');
$stmt->execute([$userId]);
$tfa = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$tfa || !$tfa['enabled']) {
$settings = $this->getUserSettings($userId);
if (!$settings) {
return false;
}
// Check if it's a backup code
// Check if code matches a backup code
if ($this->verifyBackupCode($userId, $code)) {
return true;
}
// Verify TOTP code
// Get current Unix timestamp
$currentTime = time();
// Check current and adjacent time steps
for ($timeStep = -1; $timeStep <= 1; $timeStep++) {
$checkTime = $currentTime + ($timeStep * $this->period);
if ($this->generateCode($tfa['secret_key'], $checkTime) === $code) {
// Update last used timestamp
$stmt = $this->db->prepare('
UPDATE user_2fa
SET last_used = NOW()
WHERE user_id = ?
');
$stmt->execute([$userId]);
// Check time window
for ($timeSlot = -$this->window; $timeSlot <= $this->window; $timeSlot++) {
$checkTime = $currentTime + ($timeSlot * $this->period);
$generatedCode = $this->generateCode($settings['secret_key'], $checkTime);
if (hash_equals($generatedCode, $code)) {
return true;
}
}
@ -149,49 +162,13 @@ class TwoFactorAuthentication {
}
}
/**
* Generate a TOTP code for a given secret and time
*
* @param string $secret The secret key
* @param int $time Current Unix timestamp
* @return string Generated code
*/
private function generateCode($secret, $time) {
$timeStep = floor($time / $this->period);
$timeHex = str_pad(dechex($timeStep), 16, '0', STR_PAD_LEFT);
// Convert hex time to binary
$timeBin = '';
for ($i = 0; $i < strlen($timeHex); $i += 2) {
$timeBin .= chr(hexdec(substr($timeHex, $i, 2)));
}
// Get binary secret
$secretBin = $this->base32Decode($secret);
// Calculate HMAC
$hash = hash_hmac($this->algorithm, $timeBin, $secretBin, true);
// Get offset
$offset = ord($hash[strlen($hash) - 1]) & 0xF;
// Generate 4-byte code
$code = (
((ord($hash[$offset]) & 0x7F) << 24) |
((ord($hash[$offset + 1]) & 0xFF) << 16) |
((ord($hash[$offset + 2]) & 0xFF) << 8) |
(ord($hash[$offset + 3]) & 0xFF)
) % pow(10, $this->digits);
return str_pad($code, $this->digits, '0', STR_PAD_LEFT);
}
/**
* Generate a random secret key
*
* @return string Base32 encoded secret
*/
private function generateSecret() {
// Generate random bytes (160 bits for SHA1)
$random = random_bytes($this->secretLength);
return $this->base32Encode($random);
}
@ -214,10 +191,19 @@ class TwoFactorAuthentication {
// Process 5 bits at a time
for ($i = 0; $i < strlen($binary); $i += 5) {
$chunk = substr($binary . '0000', $i, 5);
$chunk = substr($binary, $i, 5);
if (strlen($chunk) < 5) {
$chunk = str_pad($chunk, 5, '0', STR_PAD_RIGHT);
}
$encoded .= $alphabet[bindec($chunk)];
}
// Add padding
$padding = strlen($encoded) % 8;
if ($padding > 0) {
$encoded .= str_repeat('=', 8 - $padding);
}
return $encoded;
}
@ -229,16 +215,22 @@ class TwoFactorAuthentication {
*/
private function base32Decode($data) {
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
// Remove padding and uppercase
$data = rtrim(strtoupper($data), '=');
$binary = '';
$decoded = '';
// Convert to binary
for ($i = 0; $i < strlen($data); $i++) {
$position = strpos($alphabet, $data[$i]);
if ($position === false) continue;
if ($position === false) {
continue;
}
$binary .= str_pad(decbin($position), 5, '0', STR_PAD_LEFT);
}
$decoded = '';
// Process 8 bits at a time
for ($i = 0; $i + 7 < strlen($binary); $i += 8) {
$chunk = substr($binary, $i, 8);
@ -248,23 +240,60 @@ class TwoFactorAuthentication {
return $decoded;
}
/**
* Generate a TOTP code for a given secret and time
* RFC 6238 compliant implementation
*/
private function generateCode($secret, $time) {
// Calculate number of time steps since Unix epoch
$timeStep = (int)floor($time / $this->period);
// Pack time into 8 bytes (64-bit big-endian)
$timeBin = pack('J', $timeStep);
// Clean secret of any padding
$secret = rtrim($secret, '=');
// Get binary secret
$secretBin = $this->base32Decode($secret);
// Calculate HMAC
$hash = hash_hmac($this->algorithm, $timeBin, $secretBin, true);
// Get dynamic truncation offset
$offset = ord($hash[strlen($hash) - 1]) & 0xF;
// Generate 31-bit number
$code = (
((ord($hash[$offset]) & 0x7F) << 24) |
((ord($hash[$offset + 1]) & 0xFF) << 16) |
((ord($hash[$offset + 2]) & 0xFF) << 8) |
(ord($hash[$offset + 3]) & 0xFF)
) % pow(10, $this->digits);
$code = str_pad($code, $this->digits, '0', STR_PAD_LEFT);
return $code;
}
/**
* Generate otpauth URL for QR codes
*
* @param string $username Username
* @param string $secret Secret key
* @return string otpauth URL
* Format: otpauth://totp/ISSUER:ACCOUNT?secret=SECRET&issuer=ISSUER&algorithm=ALGORITHM&digits=DIGITS&period=PERIOD
*/
private function generateOtpauthUrl($username, $secret) {
$params = [
'secret' => $secret,
'issuer' => $this->issuer,
'algorithm' => strtoupper($this->algorithm),
'digits' => $this->digits,
'period' => $this->period
];
return sprintf(
'otpauth://totp/%s:%s?secret=%s&issuer=%s&algorithm=%s&digits=%d&period=%d',
urlencode($this->issuer),
urlencode($username),
$secret,
urlencode($this->issuer),
strtoupper($this->algorithm),
$this->digits,
$this->period
'otpauth://totp/%s:%s?%s',
rawurlencode($this->issuer),
rawurlencode($username),
http_build_query($params)
);
}
@ -335,11 +364,15 @@ class TwoFactorAuthentication {
*/
public function disable($userId) {
try {
// First check if user has 2FA settings
$settings = $this->getUserSettings($userId);
if (!$settings) {
return false;
}
// Delete the 2FA settings entirely instead of just disabling
$stmt = $this->db->prepare('
UPDATE user_2fa
SET enabled = 0,
secret_key = NULL,
backup_codes = NULL
DELETE FROM user_2fa
WHERE user_id = ?
');
return $stmt->execute([$userId]);
@ -368,4 +401,20 @@ class TwoFactorAuthentication {
return false;
}
}
private function getUserSettings($userId) {
try {
$stmt = $this->db->prepare('
SELECT secret_key, backup_codes, enabled
FROM user_2fa
WHERE user_id = ?
');
$stmt->execute([$userId]);
return $stmt->fetch(PDO::FETCH_ASSOC);
} catch (Exception $e) {
error_log('Failed to get user 2FA settings: ' . $e->getMessage());
return null;
}
}
}

View File

@ -507,10 +507,12 @@ class User {
* Enable two-factor authentication for a user
*
* @param int $userId User ID
* @return array Result of enabling 2FA
* @param string $secret Secret key to use
* @param string $code Verification code to validate
* @return bool True if enabled successfully
*/
public function enableTwoFactor($userId) {
return $this->twoFactorAuth->enable($userId);
public function enableTwoFactor($userId, $secret = null, $code = null) {
return $this->twoFactorAuth->enable($userId, $secret, $code);
}
/**

View File

@ -24,7 +24,7 @@ function applySessionMiddleware($config, $app_root) {
}
// Check if user is logged in
if (!isset($_SESSION['USER_ID'])) {
if (!isset($_SESSION['user_id'])) {
if (!$isTest) {
header('Location: ' . $app_root . '?page=login');
exit();

View File

@ -14,14 +14,24 @@
* - `password`: Change password
*/
// Check if user is logged in
if (!isset($_SESSION['user_id'])) {
header("Location: $app_root?page=login");
exit();
}
$user_id = $_SESSION['user_id'];
// Initialize user object
$userObject = new User($dbWeb);
$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();
$security->verifyCsrfToken($_POST['csrf_token'] ?? '');
if (!$security->verifyCsrfToken($_POST['csrf_token'] ?? '')) {
Feedback::flash('ERROR', 'DEFAULT', 'Invalid security token. Please try again.');
header("Location: $app_root?page=credentials");
@ -34,8 +44,6 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
switch ($item) {
case '2fa':
require_once '../app/helpers/2fa.php';
switch ($action) {
case 'setup':
// Validate the setup code
@ -47,13 +55,17 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
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");
// Only show error if code was actually submitted
if ($code !== '') {
Feedback::flash('ERROR', 'DEFAULT', 'Invalid verification code. Please try again.');
}
header("Location: $app_root?page=credentials&action=setup");
exit();
}
break;
case 'verify':
// This is a user-initiated verification
$code = $_POST['code'] ?? '';
if ($userObject->verifyTwoFactor($user_id, $code)) {
$_SESSION['2fa_verified'] = true;
@ -127,12 +139,16 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$has2fa = $userObject->isTwoFactorEnabled($user_id);
switch ($action) {
case 'edit':
case 'setup':
if (!$has2fa) {
require_once '../app/helpers/2fa.php';
$secret = $userObject->generateTwoFactorSecret();
$qrCode = $userObject->generateTwoFactorQR($user_id, $secret);
$backupCodes = $userObject->generateBackupCodes();
$result = $userObject->enableTwoFactor($user_id);
if ($result['success']) {
$setupData = $result['data'];
} else {
Feedback::flash('ERROR', 'DEFAULT', $result['message'] ?? 'Failed to generate 2FA setup data');
header("Location: $app_root?page=credentials");
exit();
}
}
// Get any new feedback messages
include '../app/helpers/feedback.php';

View File

@ -4,20 +4,20 @@
* User login
*
* This page ("login") handles user login, session management, cookie handling, and error logging.
* Supports "remember me" functionality to extend session duration.
* Supports "remember me" functionality to extend session duration and two-factor authentication.
*
* Actions Performed:
* - Validates login credentials.
* - Manages session and cookies based on "remember me" option.
* - Logs successful and failed login attempts.
* - Displays login form and optional custom messages.
* - Validates login credentials
* - Handles two-factor authentication if enabled
* - Manages session and cookies based on "remember me" option
* - Logs successful and failed login attempts
* - Displays login form and optional custom messages
*/
// clear the global error var before login
unset($error);
try {
// connect to database
$dbWeb = connectDB($config)['db'];
@ -28,7 +28,44 @@ try {
// Get user IP
$user_IP = getUserIP();
if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) {
$action = $_REQUEST['action'] ?? '';
if ($action === 'verify' && isset($_SESSION['2fa_pending_user_id'])) {
// Handle 2FA verification
$code = $_POST['code'] ?? '';
$userId = $_SESSION['2fa_pending_user_id'];
$username = $_SESSION['2fa_pending_username'];
$rememberMe = isset($_SESSION['2fa_pending_remember']);
require_once '../app/classes/twoFactorAuth.php';
$twoFactorAuth = new TwoFactorAuthentication($dbWeb);
if ($twoFactorAuth->verify($userId, $code)) {
// Complete login
handleSuccessfulLogin($userId, $username, $rememberMe, $config, $logObject, $user_IP);
// Clean up 2FA session data
unset($_SESSION['2fa_pending_user_id']);
unset($_SESSION['2fa_pending_username']);
unset($_SESSION['2fa_pending_remember']);
exit();
}
// If we get here (and we have code submitted), verification failed
if (!empty($code)) {
Feedback::flash('ERROR', 'DEFAULT', 'Invalid verification code');
}
// Get any new feedback messages
include '../app/helpers/feedback.php';
// Load the 2FA verification template
include '../app/templates/credentials-2fa-verify.php';
exit();
}
if ( $_SERVER['REQUEST_METHOD'] == 'POST' && $action !== 'verify' ) {
try {
// Validate form data
$security = SecurityHelper::getInstance();
@ -44,7 +81,7 @@ try {
'password' => [
'type' => 'string',
'required' => true,
'min' => 2
'min' => 5
]
];
@ -72,51 +109,35 @@ try {
$rateLimiter->attempt($username, $user_IP);
}
// login successful
if ( $userObject->login($username, $password) ) {
// if remember_me is checked, max out the session
if (isset($formData['remember_me'])) {
// 30*24*60*60 = 30 days
$cookie_lifetime = 30 * 24 * 60 * 60;
$setcookie_lifetime = time() + 30 * 24 * 60 * 60;
} else {
// 0 - session end on browser close
$cookie_lifetime = 0;
$setcookie_lifetime = 0;
// Attempt login
$loginResult = $userObject->login($username, $password);
if (is_array($loginResult)) {
switch ($loginResult['status']) {
case 'requires_2fa':
// Store pending 2FA info
$_SESSION['2fa_pending_user_id'] = $loginResult['user_id'];
$_SESSION['2fa_pending_username'] = $loginResult['username'];
if (isset($formData['remember_me'])) {
$_SESSION['2fa_pending_remember'] = true;
}
// Redirect to 2FA verification
header('Location: ?page=login&action=verify');
exit();
case 'success':
// Complete login
handleSuccessfulLogin($loginResult['user_id'], $loginResult['username'],
isset($formData['remember_me']), $config, $logObject, $user_IP);
exit();
default:
throw new Exception($loginResult['message'] ?? 'Login failed');
}
// Regenerate session ID to prevent session fixation
session_regenerate_id(true);
// set session lifetime and cookies
setcookie('username', $username, [
'expires' => $setcookie_lifetime,
'path' => $config['folder'],
'domain' => $config['domain'],
'secure' => isset($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Strict'
]);
// Set session variables
$_SESSION['USER_ID'] = $userObject->getUserId($username)[0]['id'];
$_SESSION['USERNAME'] = $username;
$_SESSION['LAST_ACTIVITY'] = time();
if (isset($formData['remember_me'])) {
$_SESSION['REMEMBER_ME'] = true;
}
// Log successful login
$user_id = $userObject->getUserId($username)[0]['id'];
$logObject->insertLog($user_id, "Login: User \"$username\" logged in. IP: $user_IP", 'user');
// Set success message and redirect
Feedback::flash('LOGIN', 'LOGIN_SUCCESS');
header('Location: ' . htmlspecialchars($app_root));
exit();
} else {
throw new Exception(Feedback::get('LOGIN', 'LOGIN_FAILED')['message']);
}
throw new Exception(Feedback::get('LOGIN', 'LOGIN_FAILED')['message']);
} catch (Exception $e) {
// Log the failed attempt
Feedback::flash('ERROR', 'DEFAULT', $e->getMessage());
@ -140,3 +161,46 @@ include '../app/helpers/feedback.php';
// Load the template
include '../app/templates/form-login.php';
/**
* Handle successful login by setting up session and cookies
*/
function handleSuccessfulLogin($userId, $username, $rememberMe, $config, $logObject, $userIP) {
if ($rememberMe) {
// 30*24*60*60 = 30 days
$cookie_lifetime = 30 * 24 * 60 * 60;
$setcookie_lifetime = time() + 30 * 24 * 60 * 60;
} else {
// 0 - session end on browser close
$cookie_lifetime = 0;
$setcookie_lifetime = 0;
}
// Regenerate session ID to prevent session fixation
session_regenerate_id(true);
// set session lifetime and cookies
setcookie('username', $username, [
'expires' => $setcookie_lifetime,
'path' => $config['folder'],
'domain' => $config['domain'],
'secure' => isset($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Strict'
]);
// Set session variables
$_SESSION['user_id'] = $userId;
$_SESSION['USERNAME'] = $username;
$_SESSION['LAST_ACTIVITY'] = time();
if ($rememberMe) {
$_SESSION['REMEMBER_ME'] = true;
}
// Log successful login
$logObject->insertLog($userId, "Login: User \"$username\" logged in. IP: $userIP", 'user');
// Set success message and redirect
Feedback::flash('LOGIN', 'LOGIN_SUCCESS');
header('Location: ' . htmlspecialchars($app_root));
}

View File

@ -22,7 +22,7 @@
</div>
<?php endif; ?>
<?php if (!empty($setupData)): ?>
<?php if (isset($setupData) && is_array($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>
@ -46,7 +46,7 @@
<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">
<form method="post" action="?page=credentials&item=2fa&action=setup" class="mt-3">
<div class="form-group">
<input type="text"
name="code"
@ -58,6 +58,7 @@
</div>
<input type="hidden" name="secret" value="<?php echo htmlspecialchars($setupData['secret']); ?>">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
<button type="submit" class="btn btn-primary mt-3">
Verify and enable 2FA
@ -82,12 +83,10 @@
</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>
<div class="alert alert-danger">
Failed to generate 2FA setup data. Please try again.
</div>
<a href="?page=credentials" class="btn btn-primary">Back to credentials</a>
<?php endif; ?>
</div>
</div>
@ -95,7 +94,7 @@
</div>
</div>
<?php if (!empty($setupData)): ?>
<?php if (isset($setupData) && is_array($setupData)): ?>
<script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {

View File

@ -20,7 +20,7 @@
<p>Enter the 6-digit code from your authenticator app:</p>
<form method="post" action="?page=credentials&action=verify" class="mt-3">
<form method="post" action="?page=login&action=verify" class="mt-3">
<div class="form-group">
<input type="text"
name="code"
@ -50,7 +50,7 @@
</p>
<div class="collapse mt-3" id="backupCodeForm">
<form method="post" action="?page=credentials&action=verify" class="mt-3">
<form method="post" action="?page=login&action=verify" class="mt-3">
<div class="form-group">
<label>Enter backup code:</label>
<input type="text"

View File

@ -77,7 +77,7 @@
<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">
<form method="post" action="?page=credentials&item=2fa&action=setup">
<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

View File

@ -29,7 +29,7 @@
<label class="form-label"><small>name:</small></label>
</div>
<div class="col-md-8 text-start bg-light">
<?= htmlspecialchars($userDetails[0]['name']) ?>
<?= htmlspecialchars($userDetails[0]['name'] ?? '') ?>
</div>
</div>
@ -38,7 +38,7 @@
<label class="form-label"><small>email:</small></label>
</div>
<div class="col-md-8 text-start bg-light">
<?= htmlspecialchars($userDetails[0]['email']) ?>
<?= htmlspecialchars($userDetails[0]['email'] ?? '') ?>
</div>
</div>