Make onboarding SMTP optional when OTP is off

This commit is contained in:
Thanakorn
2026-09-14 15:27:01 +07:00
parent d7203583b7
commit cf8106771b
2 changed files with 103 additions and 68 deletions
+24 -9
View File
@@ -19,8 +19,10 @@
* 2. CSRF check — rejects requests missing a valid X-CSRF-Token header. * 2. CSRF check — rejects requests missing a valid X-CSRF-Token header.
* 3. Decode and sanitise input fields. * 3. Decode and sanitise input fields.
* 4. Required field validation — company_name and channel_name must be non-empty. * 4. Required field validation — company_name and channel_name must be non-empty.
* 5. Required SMTP validation — smtp_host, smtp_username, smtp_password * 5. SMTP validation — smtp_host, smtp_username, smtp_password must all be
* must all be provided (company SMTP is mandatory for WMS email delivery). * provided while email OTP is on (company SMTP delivers the OTP). With
* OTP_REQUIRED=false they are optional but all-or-nothing: left blank,
* steps 6-8 and 13 are skipped and the company is created without SMTP.
* 6. Normalise smtp_port to one of ['25', '465', '587'] (default: 587). * 6. Normalise smtp_port to one of ['25', '465', '587'] (default: 587).
* Normalise smtp_encryption to one of ['tls', 'ssl', 'none'] (default: tls). * Normalise smtp_encryption to one of ['tls', 'ssl', 'none'] (default: tls).
* 7. Encrypt SMTP password with OpenSSL (same method/iv/key as rest of app). * 7. Encrypt SMTP password with OpenSSL (same method/iv/key as rest of app).
@@ -52,6 +54,7 @@ require_once '../../../session.php';
require_once '../../../config.php'; require_once '../../../config.php';
require_once '../../../dbconn.php'; require_once '../../../dbconn.php';
require_once '../../../assets/utils/db_helpers.php'; require_once '../../../assets/utils/db_helpers.php';
require_once '../../../assets/utils/otp_policy.php';
header('Content-Type: application/json; charset=utf-8'); header('Content-Type: application/json; charset=utf-8');
@@ -114,19 +117,26 @@ try {
} }
// ── Step 5: SMTP field validation ──────────────────────────────────────── // ── Step 5: SMTP field validation ────────────────────────────────────────
// SMTP is mandatory because the company needs to send OTP emails to users. // While email OTP is on, SMTP is mandatory: the company needs it to send OTP
// An account without working SMTP would be unable to complete 2FA login. // emails, and an account without working SMTP could not complete 2FA login.
// With OTP_REQUIRED=false in config.php it is optional — all three fields
// left blank means "no SMTP", and the test send (step 8) and the company_smtp
// row (step 13) are skipped. Partly filled is an error either way.
$smtp_host = trim($data['smtp_host'] ?? ''); $smtp_host = trim($data['smtp_host'] ?? '');
$smtp_username = trim($data['smtp_username'] ?? ''); $smtp_username = trim($data['smtp_username'] ?? '');
$smtp_password = $data['smtp_password'] ?? ''; $smtp_password = $data['smtp_password'] ?? '';
$smtp_given = ($smtp_host !== '' || $smtp_username !== '' || $smtp_password !== '');
if (!$smtp_host || !$smtp_username || !$smtp_password) { if ((otp_required() || $smtp_given) && (!$smtp_host || !$smtp_username || !$smtp_password)) {
$answer['message'] = 'SMTP configuration is required. Please fill in all SMTP fields.'; $answer['message'] = otp_required()
? 'SMTP configuration is required. Please fill in all SMTP fields.'
: 'Fill in SMTP host, username and password, or leave all three blank.';
http_response_code(422); http_response_code(422);
exit(json_encode($answer)); exit(json_encode($answer));
} }
// ── Step 6: Normalise SMTP port and encryption ──────────────────────────── if ($smtp_given) {
// ── Step 6: Normalise SMTP port and encryption ────────────────────────
// Clamp to known-good values to prevent storing unsupported configuration. // Clamp to known-good values to prevent storing unsupported configuration.
$smtp_port = trim($data['smtp_port'] ?? '587'); $smtp_port = trim($data['smtp_port'] ?? '587');
$smtp_encryption = trim($data['smtp_encryption'] ?? 'tls'); $smtp_encryption = trim($data['smtp_encryption'] ?? 'tls');
@@ -134,7 +144,7 @@ try {
if (!in_array($smtp_port, ['25', '465', '587'], true)) $smtp_port = '587'; if (!in_array($smtp_port, ['25', '465', '587'], true)) $smtp_port = '587';
if (!in_array($smtp_encryption, ['tls', 'ssl', 'none'], true)) $smtp_encryption = 'tls'; if (!in_array($smtp_encryption, ['tls', 'ssl', 'none'], true)) $smtp_encryption = 'tls';
// ── Step 7: Encrypt SMTP password ──────────────────────────────────────── // ── Step 7: Encrypt SMTP password ────────────────────────────────────
// Uses the same OpenSSL method/iv/key as the rest of the app (from config.php) // Uses the same OpenSSL method/iv/key as the rest of the app (from config.php)
// so the stored password can be decrypted by the mailer module. // so the stored password can be decrypted by the mailer module.
$encrypted_pass = openssl_encrypt($smtp_password, $method, $pinkey, 0, $iv); $encrypted_pass = openssl_encrypt($smtp_password, $method, $pinkey, 0, $iv);
@@ -150,7 +160,7 @@ try {
'encryption' => $smtp_encryption, 'encryption' => $smtp_encryption,
]; ];
// ── Step 8: Silent SMTP test — before any DB writes ────────────────────── // ── Step 8: Silent SMTP test — before any DB writes ──────────────────
// Sends a test email to the onboarding user's registered address. // Sends a test email to the onboarding user's registered address.
// If the mailer throws or exits, no DB records have been created yet, // If the mailer throws or exits, no DB records have been created yet,
// so the user can correct their SMTP settings and retry cleanly. // so the user can correct their SMTP settings and retry cleanly.
@@ -167,6 +177,7 @@ try {
'key' => $pinkey, 'key' => $pinkey,
]); ]);
// If mailer fails, it calls exit() internally — nothing below this line runs. // If mailer fails, it calls exit() internally — nothing below this line runs.
}
// ── Step 9: Duplicate channel_name check ───────────────────────────────── // ── Step 9: Duplicate channel_name check ─────────────────────────────────
// channel_name is the unique identifier used in URLs and API calls — must be globally unique. // channel_name is the unique identifier used in URLs and API calls — must be globally unique.
@@ -226,6 +237,9 @@ try {
// ── Step 13: Save company SMTP settings ────────────────────────────────── // ── Step 13: Save company SMTP settings ──────────────────────────────────
// Stored with the encrypted password so the mailer module can decrypt and // Stored with the encrypted password so the mailer module can decrypt and
// use it for all outgoing email from this company (OTP, notifications, etc.). // use it for all outgoing email from this company (OTP, notifications, etc.).
// Skipped when no SMTP was given (only allowed with OTP_REQUIRED=false); it
// can be added later under Settings → SMTP.
if ($smtp_given) {
$sth = $pdo1->prepare(" $sth = $pdo1->prepare("
INSERT INTO company_smtp INSERT INTO company_smtp
(company_id, server, port, username, password, (company_id, server, port, username, password,
@@ -245,6 +259,7 @@ try {
':encryption' => $smtp_encryption, ':encryption' => $smtp_encryption,
]); ]);
db_check($sth, $answer); db_check($sth, $answer);
}
// ── Step 14: Clear onboarding session keys ─────────────────────────────── // ── Step 14: Clear onboarding session keys ───────────────────────────────
// These keys are no longer needed and should not persist into the // These keys are no longer needed and should not persist into the
+27 -7
View File
@@ -1,6 +1,7 @@
<?php <?php
require '../session.php'; require '../session.php';
require '../config.php'; require '../config.php';
require_once '../assets/utils/otp_policy.php';
// Must come from email verification // Must come from email verification
if (empty($_SESSION['onboarding_user_id'])) { if (empty($_SESSION['onboarding_user_id'])) {
@@ -78,11 +79,20 @@
<div class="d-flex justify-content-between align-items-start mb-1"> <div class="d-flex justify-content-between align-items-start mb-1">
<h2 class="fs-5 mb-0"><i class="ti ti-mail-cog me-2"></i>SMTP / Email Setting</h2> <h2 class="fs-5 mb-0"><i class="ti ti-mail-cog me-2"></i>SMTP / Email Setting</h2>
<?php if (otp_required()): ?>
<span class="badge bg-label-danger">Required</span> <span class="badge bg-label-danger">Required</span>
<?php else: ?>
<span class="badge bg-label-secondary">Optional</span>
<?php endif; ?>
</div> </div>
<p class="text-muted small mb-3"> <p class="text-muted small mb-3">
<?php if (otp_required()): ?>
SMTP is required to send OTP during login. SMTP is required to send OTP during login.
A verification email will be sent when you finish setup. A verification email will be sent when you finish setup.
<?php else: ?>
Email OTP is turned off, so SMTP is optional. Leave it blank to skip;
you can add it later under Settings → SMTP.
<?php endif; ?>
</p> </p>
<!-- SMTP User Guide (collapsible) --> <!-- SMTP User Guide (collapsible) -->
@@ -175,11 +185,11 @@
<!-- SMTP Form --> <!-- SMTP Form -->
<div class="row g-3"> <div class="row g-3">
<div class="col-md-8"> <div class="col-md-8">
<label class="form-label">SMTP Host <span class="text-danger">*</span></label> <label class="form-label">SMTP Host <?php if (otp_required()): ?><span class="text-danger">*</span><?php endif; ?></label>
<input type="text" class="form-control" id="smtp_host" placeholder="e.g. smtp.gmail.com"> <input type="text" class="form-control" id="smtp_host" placeholder="e.g. smtp.gmail.com">
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label">Port <span class="text-danger">*</span></label> <label class="form-label">Port <?php if (otp_required()): ?><span class="text-danger">*</span><?php endif; ?></label>
<select class="form-select" id="smtp_port"> <select class="form-select" id="smtp_port">
<option value="587">587 — TLS</option> <option value="587">587 — TLS</option>
<option value="465">465 — SSL</option> <option value="465">465 — SSL</option>
@@ -187,11 +197,11 @@
</select> </select>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Username / Email <span class="text-danger">*</span></label> <label class="form-label">Username / Email <?php if (otp_required()): ?><span class="text-danger">*</span><?php endif; ?></label>
<input type="text" class="form-control" id="smtp_username" placeholder="your@email.com"> <input type="text" class="form-control" id="smtp_username" placeholder="your@email.com">
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Password <span class="text-danger">*</span></label> <label class="form-label">Password <?php if (otp_required()): ?><span class="text-danger">*</span><?php endif; ?></label>
<div class="input-group"> <div class="input-group">
<input type="password" class="form-control" id="smtp_password" placeholder="SMTP password"> <input type="password" class="form-control" id="smtp_password" placeholder="SMTP password">
<button class="btn btn-outline-secondary toggle-pw" type="button" data-target="smtp_password"> <button class="btn btn-outline-secondary toggle-pw" type="button" data-target="smtp_password">
@@ -252,13 +262,23 @@
return; return;
} }
if (!$('#smtp_host').val().trim() || !$('#smtp_username').val().trim() || !$('#smtp_password').val()) { // Mirrors api/engine/onboarding.php: SMTP is required while email OTP is on;
bootbox.alert('SMTP host, username and password are required.'); // with OTP_REQUIRED=false it is optional, but the three fields go together.
const smtp_required = <?php echo otp_required() ? 'true' : 'false'; ?>;
const smtp_host = $('#smtp_host').val().trim();
const smtp_user = $('#smtp_username').val().trim();
const smtp_pass = $('#smtp_password').val();
const smtp_given = !!(smtp_host || smtp_user || smtp_pass);
if ((smtp_required || smtp_given) && (!smtp_host || !smtp_user || !smtp_pass)) {
bootbox.alert(smtp_required
? 'SMTP host, username and password are required.'
: 'Fill in SMTP host, username and password, or leave all three blank.');
return; return;
} }
const $btn = $('#btn_finish'); const $btn = $('#btn_finish');
$btn.prop('disabled', true).html('<i class="ti ti-loader-2 me-1"></i>Verifying SMTP…'); $btn.prop('disabled', true).html('<i class="ti ti-loader-2 me-1"></i>' + (smtp_given ? 'Verifying SMTP…' : 'Setting up…'));
const encryption = $('input[name="smtp_encryption"]:checked').val(); const encryption = $('input[name="smtp_encryption"]:checked').val();