Files
wms-app/app/login/api/engine/register.php
T

217 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* register.php — New user registration
*
* Called by: registration page AJAX on form submission.
* Input: JSON body decoded from $_POST['json']:
* name, surname, username, email, password, confirm_password
*
* Creates a new user account in status='pending' (email not yet verified)
* and sends a 30-day email verification link. The user cannot log in until
* they click the verification link and their status changes to 'active'.
*
* Full flow:
* 1. CSRF check — rejects requests missing a valid X-CSRF-Token header.
* 2. Decode and sanitise input fields (trim, lowercase username/email).
* 3. Required field validation — all 6 fields must be non-empty.
* 4. Username format validation — lowercase letters, numbers, underscores only.
* 5. Email format validation — PHP's FILTER_VALIDATE_EMAIL.
* 6. Password match check — $password must equal $confirm_password.
* 7. Duplicate username check — 409 if already taken.
* 8. Duplicate email check — 409 if already registered.
* 9. Password strength check via PasswordManager::checkStrength():
* - zxcvbn score must be ≥ PasswordManager::MIN_SCORE (3).
* - User's own name, surname, username, email passed as penalty inputs.
* - 422 if too weak, with the first actionable zxcvbn suggestion.
* 10. Hash password with PASSWORD_BCRYPT.
* 11. Generate a 64-hex-char verification token (32 random bytes).
* 12. INSERT user row with status='pending' and the verification token.
* 13. Build absolute verify URL: <base_url>/login/verify.php?token=<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": "<reason>" }
*/
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 via system SMTP ─────────────────────
// Uses $SMTP from config.php (system-level, not company SMTP) because the
// user does not have a company yet at registration time.
// If the mailer fails it exits internally with its own error JSON response.
require_once $include_url . 'assets/utils/module/mailer.php';
$mailer = new mailer(['pdo1' => $pdo1]);
$mailer->send_email([
'company_id' => 0,
'smtp' => $SMTP,
'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:",
"",
"<a href=\"{$verify_url}\" style=\"display:inline-block;padding:12px 28px;background:#E66239;color:#ffffff;text-decoration:none;border-radius:6px;font-weight:600;\">Verify Email Address</a>",
"",
"Or copy and paste this link into your browser:",
"<a href=\"{$verify_url}\">{$verify_url}</a>",
"",
"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;
$answer['message'] = 'Account created! Please check your email to verify your account.';
} 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));