/login/verify.php?token= * 14. Send verification email via system SMTP ($SMTP from config.php). * If mailer fails, it exits internally with its own error JSON. * 15. Return { success: 1, message: "Account created! Please check your email..." } * * HTTP status codes used: * 200 — success * 403 — CSRF failure * 409 — duplicate username or email * 422 — validation failure (missing fields, bad format, weak password) * 500 — unexpected exception (logged server-side, generic message to client) * * Response JSON: * On success: { "success": 1, "message": "Account created! Please check your email to verify your account." } * On failure: { "success": 0, "message": "" } */ require '../../../session.php'; require '../../../config.php'; require '../../../dbconn.php'; require '../../../assets/utils/db_helpers.php'; require '../../../assets/utils/classes/PasswordManager.php'; header('Content-Type: application/json; charset=utf-8'); $answer = ['success' => 0, 'message' => '']; // ── Step 1: CSRF check ──────────────────────────────────────────────────────── // All POST requests must include a valid X-CSRF-Token header matching the token // stored in session. This prevents cross-site request forgery on the register form. if ($_SERVER['REQUEST_METHOD'] === 'POST') { $csrf = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? ''; if (empty($csrf) || $csrf !== ($_SESSION['csrf_token'] ?? '')) { http_response_code(403); exit(json_encode(['message' => 'Invalid request.'])); } } $data = json_decode($_POST['json'] ?? '{}', true) ?: []; try { // ── Step 2: Sanitise input ──────────────────────────────────────────────── $name = trim($data['name'] ?? ''); $surname = trim($data['surname'] ?? ''); $username = strtolower(trim($data['username'] ?? '')); $email = strtolower(trim($data['email'] ?? '')); $password = $data['password'] ?? ''; $confirm = $data['confirm_password'] ?? ''; // ── Step 3: Required field validation ──────────────────────────────────── if (!$name || !$surname || !$username || !$email || !$password || !$confirm) { $answer['message'] = 'All fields are required.'; http_response_code(422); exit(json_encode($answer)); } // ── Step 4: Username format validation ─────────────────────────────────── // Restricts usernames to URL-safe characters — prevents injection via // username in any context where it appears in a URL or query. if (!preg_match('/^[a-z0-9_]+$/', $username)) { $answer['message'] = 'Username may only contain lowercase letters, numbers and underscores.'; http_response_code(422); exit(json_encode($answer)); } // ── Step 5: Email format validation ────────────────────────────────────── if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { $answer['message'] = 'Invalid email address.'; http_response_code(422); exit(json_encode($answer)); } // ── Step 6: Password match check ───────────────────────────────────────── if ($password !== $confirm) { $answer['message'] = 'Passwords do not match.'; http_response_code(422); exit(json_encode($answer)); } // ── Step 7: Duplicate username check ───────────────────────────────────── $sth = $pdo1->prepare('SELECT user_id FROM user WHERE username = :u LIMIT 1'); $sth->execute([':u' => $username]); db_check($sth, $answer); if ($sth->fetchColumn()) { $answer['message'] = 'Username is already taken.'; http_response_code(409); exit(json_encode($answer)); } // ── Step 8: Duplicate email check ──────────────────────────────────────── $sth = $pdo1->prepare('SELECT user_id FROM user WHERE email = :e LIMIT 1'); $sth->execute([':e' => $email]); db_check($sth, $answer); if ($sth->fetchColumn()) { $answer['message'] = 'An account with that email already exists.'; http_response_code(409); exit(json_encode($answer)); } // ── Step 9: Password strength check via PasswordManager ────────────────── // Passes user's own personal data as penalty inputs so zxcvbn penalises // passwords that contain the user's name, username, or email. $pm = new PasswordManager($pdo1, $include_url); $result = $pm->checkStrength($password, [$name, $surname, $username, $email]); if ($result['score'] < PasswordManager::MIN_SCORE) { $msg = $result['warning'] ?: ($result['suggestions'][0] ?? 'Please choose a stronger password.'); $answer['message'] = 'Password is too weak. ' . $msg; http_response_code(422); exit(json_encode($answer)); } // ── Step 10–11: Hash password and generate verification token ───────────── $hashed = password_hash($password, PASSWORD_BCRYPT); $token = bin2hex(random_bytes(32)); // 64-char hex token $expires_at = date('Y-m-d H:i:s', strtotime('+30 days')); // ── Step 12: Insert user with status='pending' ──────────────────────────── // status='pending' means the account exists but cannot log in until the // email is verified. login_otp.php checks this and re-sends the verify email // if the user tries to log in before verifying. $sth = $pdo1->prepare(" INSERT INTO user (username, name, surname, email, password, status, profile_picture, verify_token, verify_expires_at) VALUES (:username, :name, :surname, :email, :password, 'pending', '', :token, :expires) "); $sth->execute([ ':username' => $username, ':name' => $name, ':surname' => $surname, ':email' => $email, ':password' => $hashed, ':token' => $token, ':expires' => $expires_at, ]); db_check($sth, $answer); // ── Step 13: Build absolute verify URL ─────────────────────────────────── // $server_url is the app's root path from config.php (e.g. '/wms'). // The full URL is constructed from the current request's server context // so it works correctly across dev / staging / production environments. $base_url = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'] . rtrim($server_url, '/'); $verify_url = $base_url . '/login/verify.php?token=' . $token; // ── Step 14: Send verification email (non-fatal) ───────────────────────── // silent=true means the mailer returns false on failure instead of calling // exit() — so the account is always created even if SMTP is unavailable. require_once $include_url . 'assets/utils/module/mailer.php'; $mailer = new mailer(['pdo1' => $pdo1]); $mail_sent = $mailer->send_email([ 'company_id' => 0, 'smtp' => $SMTP, 'silent' => true, 'to' => $email, 'subject' => 'Verify your email — WMS', 'message' => implode("\n", [ "Hi {$name},", "", "Thanks for registering. Please verify your email address by clicking the button below:", "", "Verify Email Address", "", "Or copy and paste this link into your browser:", "{$verify_url}", "", "This link will expire in 30 days.", "", "If you did not create an account, you can ignore this email.", ]), 'channel_name' => 'WMS', 'key' => $pinkey, ]); // ── Step 15: Respond ────────────────────────────────────────────────────── $answer['success'] = 1; if ($mail_sent) { $answer['message'] = 'Account created! Please check your email to verify your account.'; } else { // Email failed (e.g. SMTP blocked on this server) — account still created. // Return the verify URL so an admin can share it manually, and tell the // user to contact their administrator. $answer['message'] = 'Account created! However, the verification email could not be sent. Please contact your administrator to verify your account.'; $answer['verify_url'] = $verify_url; } } catch (Exception $e) { // Unexpected error — log details server-side, return generic message to client error_log('[register] ' . $e->getMessage()); $answer['message'] = 'Registration failed. Please try again.'; http_response_code(500); } exit(json_encode($answer));