hasRole('Admin') && !$user->hasRole('SuperUser')) {
header('Location: import_dashboard.php');
exit;
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
/*
* Default values for new users.
*/
$defaultStatus = 'Active';
/*
* Avatar upload directory.
* This path assumes this page is inside /public and avatars are stored in /upload/users.
*/
$avatarUploadDir = dirname(__DIR__) . '/upload/users/';
$avatarPublicPath = '../upload/users/';
if (!is_dir($avatarUploadDir)) {
mkdir($avatarUploadDir, 0755, true);
}
/*
* Create CSRF token.
*/
if (empty($_SESSION['user_admin_csrf'])) {
$_SESSION['user_admin_csrf'] = bin2hex(random_bytes(32));
}
$csrfToken = $_SESSION['user_admin_csrf'];
function cleanInput($value)
{
return trim((string)($value ?? ''));
}
function redirectWithMessage($type, $message)
{
$_SESSION['user_admin_flash'] = [
'type' => $type,
'message' => $message
];
header('Location: ' . $_SERVER['PHP_SELF']);
exit;
}
function getEnvValue($key, $default = '')
{
if (isset($_ENV[$key]) && $_ENV[$key] !== '') {
return $_ENV[$key];
}
$value = getenv($key);
if ($value !== false && $value !== '') {
return $value;
}
return $default;
}
function uploadAvatarIfPresent($inputName, $avatarUploadDir)
{
if (
empty($_FILES[$inputName]) ||
empty($_FILES[$inputName]['name']) ||
$_FILES[$inputName]['error'] === UPLOAD_ERR_NO_FILE
) {
return null;
}
if ($_FILES[$inputName]['error'] !== UPLOAD_ERR_OK) {
throw new Exception('Avatar upload failed.');
}
$maxSize = 3 * 1024 * 1024;
if ($_FILES[$inputName]['size'] > $maxSize) {
throw new Exception('Avatar is too large. Maximum size is 3 MB.');
}
$allowedMimeTypes = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
'image/gif' => 'gif'
];
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($_FILES[$inputName]['tmp_name']);
if (!isset($allowedMimeTypes[$mimeType])) {
throw new Exception('Invalid avatar format. Allowed formats: JPG, PNG, WEBP, GIF.');
}
$extension = $allowedMimeTypes[$mimeType];
$filename = 'user_' . time() . '_' . bin2hex(random_bytes(6)) . '.' . $extension;
$destination = rtrim($avatarUploadDir, '/') . '/' . $filename;
if (!move_uploaded_file($_FILES[$inputName]['tmp_name'], $destination)) {
throw new Exception('Unable to save avatar.');
}
return $filename;
}
function isRoleAllowedForManagement(PDO $pdo, $roleId)
{
/*
* Admin role cannot be assigned from this page.
*/
$stmt = $pdo->prepare("
SELECT id
FROM auth_roles
WHERE id = :id
AND name <> 'Admin'
LIMIT 1
");
$stmt->execute([':id' => (int)$roleId]);
return (bool)$stmt->fetch(PDO::FETCH_ASSOC);
}
function isUserAdmin(PDO $pdo, $userId)
{
$stmt = $pdo->prepare("
SELECT u.id
FROM auth_users u
INNER JOIN auth_roles r ON r.id = u.role_id
WHERE u.id = :id
AND r.name = 'Admin'
LIMIT 1
");
$stmt->execute([':id' => (int)$userId]);
return (bool)$stmt->fetch(PDO::FETCH_ASSOC);
}
function smtpReadLine($socket)
{
$response = '';
while ($line = fgets($socket, 515)) {
$response .= $line;
if (isset($line[3]) && $line[3] === ' ') {
break;
}
}
return $response;
}
function smtpCommand($socket, $command, array $expectedCodes)
{
if ($command !== null) {
fwrite($socket, $command . "\r\n");
}
$response = smtpReadLine($socket);
$code = (int)substr($response, 0, 3);
if (!in_array($code, $expectedCodes, true)) {
throw new Exception('SMTP error after command [' . $command . ']: ' . trim($response));
}
return $response;
}
function buildSmartTrfWelcomeEmailHtml($firstName, $lastName, $email, $temporaryPassword, $loginUrl)
{
$fullName = trim($firstName . ' ' . $lastName);
$safeFullName = htmlspecialchars($fullName, ENT_QUOTES, 'UTF-8');
$safeEmail = htmlspecialchars($email, ENT_QUOTES, 'UTF-8');
$safePassword = htmlspecialchars($temporaryPassword, ENT_QUOTES, 'UTF-8');
$safeLoginUrl = htmlspecialchars($loginUrl, ENT_QUOTES, 'UTF-8');
return '
Welcome to SmartTRF
|
SmartTRF
Your digital workspace for sample and TRF management
|
Welcome, ' . $safeFullName . '
Your SmartTRF account has been created. You can now access the platform using the credentials below.
|
|
Access credentials
| Email |
' . $safeEmail . ' |
| Temporary password |
' . $safePassword . ' |
|
|
Security recommendation
This is a temporary password. For security reasons, please change it after your first login.
|
|
Access SmartTRF
If the button does not work, copy and paste this link into your browser:
' . $safeLoginUrl . '
|
|
This message was automatically generated by SmartTRF. Please do not reply to this email.
|
SmartTRF ยท C.E. Soft
|
';
}
function buildSmartTrfWelcomeEmailText($firstName, $lastName, $email, $temporaryPassword, $loginUrl)
{
$fullName = trim($firstName . ' ' . $lastName);
return "Welcome to SmartTRF\n\n" .
"Hello " . $fullName . ",\n\n" .
"Your SmartTRF account has been created.\n\n" .
"Access credentials:\n" .
"Email: " . $email . "\n" .
"Temporary password: " . $temporaryPassword . "\n\n" .
"For security reasons, please change this temporary password after your first login.\n\n" .
"Login URL:\n" .
$loginUrl . "\n\n" .
"This message was automatically generated by SmartTRF.";
}
function sendSmartTrfWelcomeEmail($toEmail, $firstName, $lastName, $temporaryPassword)
{
$smtpHost = getEnvValue('MAIL_HOST');
$smtpPort = (int)getEnvValue('MAIL_PORT', 465);
$smtpUsername = getEnvValue('MAIL_USERNAME');
$smtpPassword = getEnvValue('MAIL_PASSWORD');
$smtpEncryption = strtolower(getEnvValue('MAIL_ENCRYPTION', 'ssl'));
$fromAddress = getEnvValue('MAIL_FROM_ADDRESS', $smtpUsername);
$fromName = getEnvValue('MAIL_FROM_NAME', 'SmartTRF');
$loginUrl = getEnvValue('APP_URL', 'https://trf.cesoft.io/public/login');
if ($smtpHost === '' || $smtpUsername === '' || $smtpPassword === '' || $fromAddress === '') {
throw new Exception('SMTP configuration is incomplete.');
}
$subject = 'Welcome to SmartTRF - Your account has been created';
$htmlBody = buildSmartTrfWelcomeEmailHtml($firstName, $lastName, $toEmail, $temporaryPassword, $loginUrl);
$textBody = buildSmartTrfWelcomeEmailText($firstName, $lastName, $toEmail, $temporaryPassword, $loginUrl);
$boundary = 'smarttrf_' . bin2hex(random_bytes(12));
$encodedSubject = '=?UTF-8?B?' . base64_encode($subject) . '?=';
$encodedFromName = '=?UTF-8?B?' . base64_encode($fromName) . '?=';
$headers = [];
$headers[] = 'From: ' . $encodedFromName . ' <' . $fromAddress . '>';
$headers[] = 'To: <' . $toEmail . '>';
$headers[] = 'Subject: ' . $encodedSubject;
$headers[] = 'MIME-Version: 1.0';
$headers[] = 'Content-Type: multipart/alternative; boundary="' . $boundary . '"';
$headers[] = 'Date: ' . date('r');
$headers[] = 'Message-ID: <' . bin2hex(random_bytes(16)) . '@smarttrf>';
$message = '';
$message .= '--' . $boundary . "\r\n";
$message .= "Content-Type: text/plain; charset=UTF-8\r\n";
$message .= "Content-Transfer-Encoding: 8bit\r\n\r\n";
$message .= $textBody . "\r\n\r\n";
$message .= '--' . $boundary . "\r\n";
$message .= "Content-Type: text/html; charset=UTF-8\r\n";
$message .= "Content-Transfer-Encoding: 8bit\r\n\r\n";
$message .= $htmlBody . "\r\n\r\n";
$message .= '--' . $boundary . "--\r\n";
$smtpMessage = implode("\r\n", $headers) . "\r\n\r\n" . $message;
$transportHost = ($smtpEncryption === 'ssl' ? 'ssl://' : '') . $smtpHost;
$socket = stream_socket_client(
$transportHost . ':' . $smtpPort,
$errno,
$errstr,
30,
STREAM_CLIENT_CONNECT
);
if (!$socket) {
throw new Exception('SMTP connection failed: ' . $errstr . ' (' . $errno . ')');
}
stream_set_timeout($socket, 30);
try {
smtpCommand($socket, null, [220]);
smtpCommand($socket, 'EHLO ' . ($_SERVER['SERVER_NAME'] ?? 'localhost'), [250]);
if ($smtpEncryption === 'tls') {
smtpCommand($socket, 'STARTTLS', [220]);
if (!stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
throw new Exception('Unable to enable TLS encryption.');
}
smtpCommand($socket, 'EHLO ' . ($_SERVER['SERVER_NAME'] ?? 'localhost'), [250]);
}
smtpCommand($socket, 'AUTH LOGIN', [334]);
smtpCommand($socket, base64_encode($smtpUsername), [334]);
smtpCommand($socket, base64_encode($smtpPassword), [235]);
smtpCommand($socket, 'MAIL FROM:<' . $fromAddress . '>', [250]);
smtpCommand($socket, 'RCPT TO:<' . $toEmail . '>', [250, 251]);
smtpCommand($socket, 'DATA', [354]);
fwrite($socket, $smtpMessage . "\r\n.\r\n");
smtpCommand($socket, null, [250]);
smtpCommand($socket, 'QUIT', [221]);
} finally {
fclose($socket);
}
return true;
}
/*
* Load roles available for creation/edit.
* Admin is intentionally excluded.
*/
$stmtRoles = $pdo->query("
SELECT id, name, display_name
FROM auth_roles
WHERE name <> 'Admin'
ORDER BY display_name ASC, name ASC
");
$manageableRoles = $stmtRoles->fetchAll(PDO::FETCH_ASSOC);
if (empty($manageableRoles)) {
redirectWithMessage('error', 'No manageable roles found. Please create at least one non-admin role.');
}
$defaultRoleId = (int)$manageableRoles[0]['id'];
/*
* Handle form actions.
*/
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$postedToken = $_POST['csrf_token'] ?? '';
if (!hash_equals($csrfToken, $postedToken)) {
throw new Exception('Invalid security token.');
}
$action = cleanInput($_POST['action'] ?? '');
if ($action === 'create') {
$firstName = cleanInput($_POST['first_name'] ?? '');
$lastName = cleanInput($_POST['last_name'] ?? '');
$email = cleanInput($_POST['email'] ?? '');
$password = (string)($_POST['password'] ?? '');
$roleId = (int)($_POST['role_id'] ?? 0);
$limsUserId = cleanInput($_POST['lims_user_id'] ?? '');
$limsGlobalUserId = cleanInput($_POST['lims_global_user_id'] ?? '');
if ($firstName === '' || $lastName === '' || $email === '' || $password === '' || $roleId <= 0) {
throw new Exception('Please fill all required fields.');
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new Exception('Invalid email address.');
}
if (!isRoleAllowedForManagement($pdo, $roleId)) {
throw new Exception('You cannot assign the Admin role from this page.');
}
$stmt = $pdo->prepare("SELECT id FROM auth_users WHERE email = :email LIMIT 1");
$stmt->execute([':email' => $email]);
if ($stmt->fetch()) {
throw new Exception('This email already exists.');
}
$avatarFilename = uploadAvatarIfPresent('avatar', $avatarUploadDir);
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
$sql = "
INSERT INTO auth_users
(
email,
username,
password,
first_name,
last_name,
avatar,
role_id,
status,
lims_user_id,
lims_global_user_id,
created_at,
updated_at
)
VALUES
(
:email,
NULL,
:password,
:first_name,
:last_name,
:avatar,
:role_id,
:status,
:lims_user_id,
:lims_global_user_id,
NOW(),
NOW()
)
";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':email' => $email,
':password' => $passwordHash,
':first_name' => $firstName,
':last_name' => $lastName,
':avatar' => $avatarFilename,
':role_id' => $roleId,
':status' => $defaultStatus,
':lims_user_id' => ($limsUserId !== '' ? (int)$limsUserId : null),
':lims_global_user_id' => ($limsGlobalUserId !== '' ? (int)$limsGlobalUserId : null)
]);
try {
sendSmartTrfWelcomeEmail($email, $firstName, $lastName, $password);
redirectWithMessage('success', 'User created successfully. Welcome email sent.');
} catch (Exception $mailException) {
error_log('SmartTRF welcome email error: ' . $mailException->getMessage());
redirectWithMessage('warning', 'User created successfully, but the welcome email was not sent: ' . $mailException->getMessage());
}
}
if ($action === 'update') {
$userId = (int)($_POST['user_id'] ?? 0);
$firstName = cleanInput($_POST['first_name'] ?? '');
$lastName = cleanInput($_POST['last_name'] ?? '');
$email = cleanInput($_POST['email'] ?? '');
$password = (string)($_POST['password'] ?? '');
$roleId = (int)($_POST['role_id'] ?? 0);
$limsUserId = cleanInput($_POST['lims_user_id'] ?? '');
$limsGlobalUserId = cleanInput($_POST['lims_global_user_id'] ?? '');
if ($userId <= 0) {
throw new Exception('Invalid user ID.');
}
if (isUserAdmin($pdo, $userId)) {
throw new Exception('Admin users can be viewed but cannot be edited from this page.');
}
if ($firstName === '' || $lastName === '' || $email === '' || $roleId <= 0) {
throw new Exception('Please fill all required fields.');
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new Exception('Invalid email address.');
}
if (!isRoleAllowedForManagement($pdo, $roleId)) {
throw new Exception('You cannot assign the Admin role from this page.');
}
$stmt = $pdo->prepare("SELECT id FROM auth_users WHERE email = :email AND id <> :id LIMIT 1");
$stmt->execute([
':email' => $email,
':id' => $userId
]);
if ($stmt->fetch()) {
throw new Exception('This email is already used by another user.');
}
$avatarFilename = uploadAvatarIfPresent('avatar', $avatarUploadDir);
$fields = [
'email = :email',
'first_name = :first_name',
'last_name = :last_name',
'role_id = :role_id',
'lims_user_id = :lims_user_id',
'lims_global_user_id = :lims_global_user_id',
'updated_at = NOW()'
];
$params = [
':email' => $email,
':first_name' => $firstName,
':last_name' => $lastName,
':role_id' => $roleId,
':lims_user_id' => ($limsUserId !== '' ? (int)$limsUserId : null),
':lims_global_user_id' => ($limsGlobalUserId !== '' ? (int)$limsGlobalUserId : null),
':id' => $userId
];
if ($avatarFilename !== null) {
$fields[] = 'avatar = :avatar';
$params[':avatar'] = $avatarFilename;
}
if ($password !== '') {
$fields[] = 'password = :password';
$params[':password'] = password_hash($password, PASSWORD_DEFAULT);
}
$sql = "UPDATE auth_users SET " . implode(', ', $fields) . " WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
redirectWithMessage('success', 'User updated successfully.');
}
if ($action === 'delete') {
$userId = (int)($_POST['user_id'] ?? 0);
if ($userId <= 0) {
throw new Exception('Invalid user ID.');
}
if (isset($iduserlogin) && (int)$iduserlogin === $userId) {
throw new Exception('You cannot delete your own user.');
}
if (isUserAdmin($pdo, $userId)) {
throw new Exception('Admin users can be viewed but cannot be deleted from this page.');
}
$stmt = $pdo->prepare("DELETE FROM auth_users WHERE id = :id");
$stmt->execute([':id' => $userId]);
redirectWithMessage('success', 'User deleted successfully.');
}
throw new Exception('Invalid action.');
} catch (Exception $e) {
redirectWithMessage('error', $e->getMessage());
}
}
/*
* Load users with role information.
*/
$stmt = $pdo->query("
SELECT
u.id,
u.first_name,
u.last_name,
u.email,
u.avatar,
u.status,
u.role_id,
u.lims_user_id,
u.lims_global_user_id,
u.created_at,
u.updated_at,
r.name AS role_name,
r.display_name AS role_display_name
FROM auth_users u
LEFT JOIN auth_roles r ON r.id = u.role_id
ORDER BY u.first_name ASC, u.last_name ASC, u.email ASC
");
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
$flash = $_SESSION['user_admin_flash'] ?? null;
unset($_SESSION['user_admin_flash']);
?>
User Administration - = htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?>
| Avatar |
First Name |
Last Name |
Email |
Role |
Accettatore |
LIMS Global |
Status |
Actions |
|
= htmlspecialchars($userRow['first_name'] ?? '', ENT_QUOTES, 'UTF-8'); ?> |
= htmlspecialchars($userRow['last_name'] ?? '', ENT_QUOTES, 'UTF-8'); ?> |
= htmlspecialchars($userRow['email'] ?? '', ENT_QUOTES, 'UTF-8'); ?> |
= htmlspecialchars($roleDisplayName ?? '', ENT_QUOTES, 'UTF-8'); ?>
|
= htmlspecialchars($userRow['lims_user_id'] ?? '', ENT_QUOTES, 'UTF-8'); ?>
|
= htmlspecialchars($userRow['lims_global_user_id'] ?? '', ENT_QUOTES, 'UTF-8'); ?>
|
= htmlspecialchars($userRow['status'] ?? '', ENT_QUOTES, 'UTF-8'); ?>
|
|
Loading LIMS users and acceptors...