" } */ require_once '../../../session.php'; require_once '../../../config.php'; require_once '../../../dbconn.php'; require_once '../../../assets/utils/db_helpers.php'; require_once '../../../assets/utils/otp_policy.php'; require_once '../../../assets/utils/rate_limit.php'; require_once '../../../assets/utils/secret_box.php'; header('Content-Type: application/json; charset=utf-8'); $answer = ['success' => 0, 'message' => '']; // ── Step 1: Session guard ───────────────────────────────────────────────────── // 'onboarding_user_id' is only written by verify.php after successful email // verification. If it's missing, this request is out-of-sequence — reject. if (empty($_SESSION['onboarding_user_id'])) { $answer['message'] = 'Invalid session. Please verify your email first.'; http_response_code(403); exit(json_encode($answer)); } $user_id = (int)$_SESSION['onboarding_user_id']; // ── Step 1b: License guard ──────────────────────────────────────────────────── // Invited users (license='user') must use invited_onboarding.php, not this flow. // If somehow an invited user's session reaches here, reject immediately. $sth = $pdo1->prepare("SELECT license FROM user WHERE user_id = :uid LIMIT 1"); $sth->execute([':uid' => $user_id]); if ($sth->fetchColumn() !== 'owner') { $answer['message'] = 'Invalid session.'; http_response_code(403); exit(json_encode($answer)); } // ── Step 2: CSRF check ──────────────────────────────────────────────────────── if ($_SERVER['REQUEST_METHOD'] === 'POST') { $csrf = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? ''; if (empty($csrf) || !hash_equals((string)($_SESSION['csrf_token'] ?? ''), $csrf)) { http_response_code(403); $answer['message'] = 'Invalid request.'; exit(json_encode($answer)); } } $data = json_decode($_POST['json'] ?? '{}', true) ?: []; // ── Step 2b: Throttle — each attempt sends an SMTP test email ──────────────── rate_limit_guard($pdo1, [ ['onboarding_ip', rate_limit_client_ip(), 20, 900], ['onboarding_user', (string)$user_id, 10, 900], ]); try { // ── Step 3: Sanitise input ──────────────────────────────────────────────── $company_name = trim($data['company_name'] ?? ''); $company_name2 = trim($data['company_name2'] ?? ''); // channel_name is the URL slug / identifier — lowercase first, then strip // everything except lowercase letters, digits, hyphens, and underscores. $channel_name = preg_replace('/[^a-z0-9\-_]/', '', strtolower(trim($data['channel_name'] ?? ''))); $branch = trim($data['branch'] ?? 'สำนักงานใหญ่'); $branch_no = trim($data['branch_no'] ?? '00000'); $email = trim($data['email'] ?? ''); $phone = trim($data['phone'] ?? ''); // ── Step 4: Required field validation ──────────────────────────────────── if (!$company_name || !$channel_name) { $answer['message'] = 'Company name and channel name are required.'; http_response_code(422); exit(json_encode($answer)); } // ── Step 5: SMTP field validation ──────────────────────────────────────── // While email OTP is on, SMTP is mandatory: the company needs it to send OTP // 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_username = trim($data['smtp_username'] ?? ''); $smtp_password = $data['smtp_password'] ?? ''; $smtp_given = ($smtp_host !== '' || $smtp_username !== '' || $smtp_password !== ''); if ((otp_required() || $smtp_given) && (!$smtp_host || !$smtp_username || !$smtp_password)) { $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); exit(json_encode($answer)); } if ($smtp_given) { // ── Step 6: Normalise SMTP port and encryption ──────────────────────── // Clamp to known-good values to prevent storing unsupported configuration. $smtp_port = trim($data['smtp_port'] ?? '587'); $smtp_encryption = trim($data['smtp_encryption'] ?? 'tls'); if (!in_array($smtp_port, ['25', '465', '587'], true)) $smtp_port = '587'; if (!in_array($smtp_encryption, ['tls', 'ssl', 'none'], true)) $smtp_encryption = 'tls'; // ── Step 7: Encrypt SMTP password ──────────────────────────────────── // 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. $encrypted_pass = secret_encrypt($smtp_password, $pinkey); // Assemble a temporary SMTP config for the test send (step 8) $smtp_config = [ 'server' => $smtp_host, 'port' => $smtp_port, 'username' => $smtp_username, 'password' => $encrypted_pass, 'from_name' => $company_name ?: $smtp_username, 'from_email' => $email ?: $smtp_username, 'encryption' => $smtp_encryption, ]; // ── Step 8: Silent SMTP test — before any DB writes ────────────────── // Sends a test email to the onboarding user's registered address. // If the mailer throws or exits, no DB records have been created yet, // so the user can correct their SMTP settings and retry cleanly. require_once '../../../assets/utils/module/mailer.php'; $mailer = new mailer(['pdo1' => $pdo1]); $mailer->send_email([ 'company_id' => 0, 'smtp' => $smtp_config, 'to' => $_SESSION['onboarding_email'] ?? $smtp_username, 'subject' => 'WMS — SMTP Verification', 'message' => "Your SMTP is working correctly.\n\nSetup is now complete.", 'channel_name' => $company_name ?: 'WMS', 'key' => $pinkey, ]); // If mailer fails, it calls exit() internally — nothing below this line runs. } // ── Step 9: Duplicate channel_name check ───────────────────────────────── // channel_name is the unique identifier used in URLs and API calls — must be globally unique. $sth = $pdo1->prepare('SELECT company_id FROM company_list WHERE channel_name = :c LIMIT 1'); $sth->execute([':c' => $channel_name]); db_check($sth, $answer); if ($sth->fetchColumn()) { $answer['message'] = 'Channel name is already taken. Please choose another.'; http_response_code(409); exit(json_encode($answer)); } // ── Step 10: Create company record ─────────────────────────────────────── // fx (currency) defaults to 'thb' — can be changed later in company settings. $sth = $pdo1->prepare(" INSERT INTO company_list (channel_name, company_name, company_name2, branch, branch_no, email, phone, fx) VALUES (:channel_name, :company_name, :company_name2, :branch, :branch_no, :email, :phone, 'thb') "); $sth->execute([ ':channel_name' => $channel_name, ':company_name' => $company_name, ':company_name2' => $company_name2, ':branch' => $branch, ':branch_no' => $branch_no, ':email' => $email, ':phone' => $phone, ]); db_check($sth, $answer); $company_id = (int)$pdo1->lastInsertId(); // ── Step 11: Map user as company owner ─────────────────────────────────── // company_map_user is the many-to-many table between users and companies. // 'owner' role grants full admin access within the company. // app_access mirrors the owner's license (user.app_access) so the column // is never NULL and switch_branch reads consistent data. $sth = $pdo1->prepare("SELECT app_access FROM user WHERE user_id = :u LIMIT 1"); $sth->execute([':u' => $user_id]); $owner_app_access = $sth->fetchColumn() ?: 'wms'; $sth = $pdo1->prepare(" INSERT INTO company_map_user (company_id, user_id, role, app_access, created_at) VALUES (:company_id, :user_id, 'owner', :app_access, NOW()) "); $sth->execute([':company_id' => $company_id, ':user_id' => $user_id, ':app_access' => $owner_app_access]); db_check($sth, $answer); // ── Step 12: Activate user account and set default company ─────────────── // Changing status from 'pending' to 'active' lets login_otp.php proceed // past the unverified-account check. default_company scopes all DB queries // after login to this company. $sth = $pdo1->prepare("UPDATE user SET default_company = :c, `status` = 'active' WHERE user_id = :u"); $sth->execute([':c' => $company_id, ':u' => $user_id]); db_check($sth, $answer); // ── Step 13: Save company SMTP settings ────────────────────────────────── // Stored with the encrypted password so the mailer module can decrypt and // 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(" INSERT INTO company_smtp (company_id, server, port, username, password, from_name, from_email, encryption, updated_at) VALUES (:company_id, :server, :port, :username, :password, :from_name, :from_email, :encryption, NOW()) "); $sth->execute([ ':company_id' => $company_id, ':server' => $smtp_host, ':port' => $smtp_port, ':username' => $smtp_username, ':password' => $encrypted_pass, ':from_name' => $company_name, ':from_email' => $email ?: $smtp_username, ':encryption' => $smtp_encryption, ]); db_check($sth, $answer); } // ── Step 14: Clear onboarding session keys ─────────────────────────────── // These keys are no longer needed and should not persist into the // authenticated session. The user will be redirected to the login page. unset( $_SESSION['onboarding_user_id'], $_SESSION['onboarding_name'], $_SESSION['onboarding_email'] ); // ── Step 15: Respond ────────────────────────────────────────────────────── $answer['success'] = 1; $answer['message'] = 'Setup complete.'; } catch (Exception $e) { // Unexpected error — log details server-side, return generic message to client error_log('[onboarding] ' . $e->getMessage()); $answer['message'] = 'Setup failed. Please try again.'; http_response_code(500); } exit(json_encode($answer));