Fix mail config not reaching vm420 + let admins view/set a user's email

docker-compose.yml (the file vm420 actually runs, not
docker-compose.prod.yml) hardcoded MAIL_HOST=mailhog and
MAIL_SMTP_AUTH=false with no way to override from .env, and never
passed MAIL_USERNAME/MAIL_PASSWORD through at all - so setting real
values in .env silently had no effect on the running container.
Registrations succeeded and created accounts, but the password email
was never actually sent (PHPMailer tried to reach a nonexistent
"mailhog" host inside vm420's network). Switched to
${MAIL_HOST:-mailhog}-style fallbacks, same pattern already used for
DATABASE_PASSWORD, so local dev still defaults to mailhog for free
while a real .env override now actually takes effect.

Separately: the admin-panel user form never had an email field at
all (only register.php could set one) - a superadmin had no way to
view or correct a self-registered user's email address. Added the
field (optional, uniqueness-checked, defaults to forcing
must_set_email on next login if left blank) to form_users.php,
Users::addUser()/editUser(), and the users list/table.
This commit is contained in:
2026-07-14 12:47:28 +02:00
parent a692304748
commit 9f10c8ffb5
5 changed files with 88 additions and 7 deletions
+11 -4
View File
@@ -16,10 +16,17 @@ services:
DATABASE_PREFIX: "${DATABASE_PREFIX:-}"
DATABASE_CHARSET: "${DATABASE_CHARSET:-utf8}"
ALLOW_SELF_REGISTRATION: "${ALLOW_SELF_REGISTRATION:-false}"
MAIL_HOST: "mailhog"
MAIL_PORT: "1025"
MAIL_ENCRYPTION: ""
MAIL_SMTP_AUTH: "false"
# This file is also what vm420 runs in production (not docker-compose.prod.yml -
# see the "prod" naming is misleading, this is the actually-deployed one). Defaults
# here are dev-convenience only (mailhog, no auth) - a real deployment's .env must
# override MAIL_HOST/MAIL_SMTP_AUTH/MAIL_USERNAME/MAIL_PASSWORD explicitly, same as
# DATABASE_PASSWORD above already requires.
MAIL_HOST: "${MAIL_HOST:-mailhog}"
MAIL_PORT: "${MAIL_PORT:-1025}"
MAIL_ENCRYPTION: "${MAIL_ENCRYPTION:-}"
MAIL_SMTP_AUTH: "${MAIL_SMTP_AUTH:-false}"
MAIL_USERNAME: "${MAIL_USERNAME:-}"
MAIL_PASSWORD: "${MAIL_PASSWORD:-}"
MAIL_FROM_ADDRESS: "${MAIL_FROM_ADDRESS:-noreply@example.com}"
MAIL_FROM_NAME: "${MAIL_FROM_NAME:-QRForge}"
ports:
+14
View File
@@ -12,6 +12,20 @@
</div>
</div>
<div class="col-sm-4">
<div class="form-group">
<label for="email">Email</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-envelope"></i></span>
</div>
<input type="email" name="email" placeholder="Email" class="form-control" value="<?php echo ($edit) ? htmlspecialchars($user['email'] ?? '', ENT_QUOTES, 'UTF-8') : ''; ?>" autocomplete="off">
</div>
<small class="form-text text-muted">Used to log in once set. Leave blank to prompt for it on next login.</small>
</div>
</div>
<div class="col-sm-4">
<div class="form-group">
<label for="password">Password *</label>
+4 -2
View File
@@ -6,8 +6,9 @@
<thead>
<tr>
<th width="5%">ID</th>
<th width="45%">Username</th>
<th width="40%">Type</th>
<th width="25%">Username</th>
<th width="30%">Email</th>
<th width="30%">Type</th>
<th width="10%">Actions</th>
</tr>
</thead>
@@ -16,6 +17,7 @@
<tr>
<td><?php echo $row['id']; ?></td>
<td><?php echo htmlspecialchars($row['username']); ?></td>
<td><?php echo htmlspecialchars($row['email'] ?? ''); ?></td>
<td><?php echo htmlspecialchars($row['type']); ?></td>
<td>
<!-- EDIT -->
+58
View File
@@ -32,6 +32,23 @@ class Users
return null;
}
/**
* Server-side validation of the (optional, for admin-created accounts) email address.
* Empty is allowed here - an account without one gets must_set_email=1, same as a
* pre-migration legacy account (see the callers below).
*/
private function validateEmail($email) {
if ($email === '' || $email === null) {
return null;
}
if (!is_string($email) || strlen($email) > 255 || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
return 'Please enter a valid email address.';
}
return null;
}
/**
*
*/
@@ -114,11 +131,19 @@ class Users
$this->failure($validation_error, 'Location: user.php');
}
$email = trim($input_data['email'] ?? '');
$email_error = $this->validateEmail($email);
if ($email_error !== null) {
$this->failure($email_error, 'Location: user.php');
}
if (!isset($input_data['password']) || strlen($input_data['password']) < 10) {
$this->failure('Password must be at least 10 characters long.', 'Location: user.php');
}
$data_to_db["username"] = $input_data["username"];
$data_to_db["email"] = $email !== '' ? $email : null;
$data_to_db['must_set_email'] = $email === '' ? 1 : 0;
$data_to_db['password'] = password_hash($input_data['password'], PASSWORD_DEFAULT);
$data_to_db["type"] = $requested_type;
$data_to_db['owner_admin_id'] = $owner_admin_id;
@@ -131,6 +156,16 @@ class Users
if ($db->count >= 1)
$this->failure('Username already exists');
if ($email !== '') {
$db = getDbInstance();
$db->where('email', $email);
$db->get('users');
if ($db->count >= 1)
$this->failure('An account with this email already exists', 'Location: user.php');
}
$db = getDbInstance();
$last_id = $db->insert('users', $data_to_db);
if ($last_id) {
@@ -258,6 +293,12 @@ class Users
$this->failure($validation_error, 'Location: user.php?'.$query_string);
}
$email = trim($input_data['email'] ?? '');
$email_error = $this->validateEmail($email);
if ($email_error !== null) {
$this->failure($email_error, 'Location: user.php?'.$query_string);
}
if (isset($input_data['password']) && strlen($input_data['password']) > 0 && strlen($input_data['password']) < 10) {
$this->failure('Password must be at least 10 characters long.', 'Location: user.php?'.$query_string);
}
@@ -271,8 +312,25 @@ class Users
$this->failure('Username already exists', 'Location: user.php?'.$query_string);
}
if ($email !== '') {
$db = getDbInstance();
$db->where('email', $email);
$db->where('id', $input_data["id"], '!=');
$row = $db->getOne('users');
if (!empty($row['email'])) {
$this->failure('An account with this email already exists', 'Location: user.php?'.$query_string);
}
}
$data_to_db["username"] = $input_data["username"];
$data_to_db["type"] = $requested_type;
// Only touch email/must_set_email if an email was actually submitted - an admin
// leaving the field blank on an already-set account shouldn't wipe it back out.
if ($email !== '') {
$data_to_db['email'] = $email;
$data_to_db['must_set_email'] = 0;
}
$data_to_db['can_view_static'] = !empty($input_data['can_view_static']) ? 1 : 0;
$data_to_db['can_view_dynamic'] = !empty($input_data['can_view_dynamic']) ? 1 : 0;
+1 -1
View File
@@ -9,7 +9,7 @@ $users = new Users();
if (!in_array($_SESSION['type'], ['super', 'admin'], true))
$users->failure('Only "super admin" and "admin" accounts can access the user management page', 'Location: index.php');
$select = array('id', 'username', 'type');
$select = array('id', 'username', 'email', 'type');
$search_fields = array('username');
require_once BASE_PATH . '/includes/search_order.php';
$page = filter_input(INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?? 1;