modify classed and comments
This commit is contained in:
+209
-148
@@ -1,156 +1,217 @@
|
||||
<?php
|
||||
require '../../../session.php';
|
||||
require '../../../config.php';
|
||||
require '../../../dbconn.php';
|
||||
require '../../../assets/utils/db_helpers.php';
|
||||
require '../../../assets/utils/classes/PasswordManager.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>" }
|
||||
*/
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
require '../../../session.php';
|
||||
require '../../../config.php';
|
||||
require '../../../dbconn.php';
|
||||
require '../../../assets/utils/db_helpers.php';
|
||||
require '../../../assets/utils/classes/PasswordManager.php';
|
||||
|
||||
$answer = ['success' => 0, 'message' => ''];
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
// ─── CSRF ─────────────────────────────────────────────────────────────────
|
||||
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.']));
|
||||
}
|
||||
$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));
|
||||
}
|
||||
|
||||
$data = json_decode($_POST['json'] ?? '{}', true) ?: [];
|
||||
|
||||
try {
|
||||
|
||||
$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'] ?? '';
|
||||
|
||||
// ── Required fields ───────────────────────────────────────
|
||||
if (!$name || !$surname || !$username || !$email || !$password || !$confirm) {
|
||||
$answer['message'] = 'All fields are required.';
|
||||
http_response_code(422);
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
// ── Username format ───────────────────────────────────────
|
||||
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));
|
||||
}
|
||||
|
||||
// ── Email format ──────────────────────────────────────────
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$answer['message'] = 'Invalid email address.';
|
||||
http_response_code(422);
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
// ── Password match ────────────────────────────────────────
|
||||
if ($password !== $confirm) {
|
||||
$answer['message'] = 'Passwords do not match.';
|
||||
http_response_code(422);
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
// ── Duplicate username ────────────────────────────────────
|
||||
$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));
|
||||
}
|
||||
|
||||
// ── Duplicate email ───────────────────────────────────────
|
||||
$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));
|
||||
}
|
||||
|
||||
// ── Password strength ─────────────────────────────────────
|
||||
$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));
|
||||
}
|
||||
|
||||
// ── Insert user with status=pending ───────────────────────
|
||||
$hashed = password_hash($password, PASSWORD_BCRYPT);
|
||||
$token = bin2hex(random_bytes(32));
|
||||
|
||||
$expires_at = date('Y-m-d H:i:s', strtotime('+30 days'));
|
||||
|
||||
$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);
|
||||
|
||||
// ── Send verification email via default SMTP ──────────────
|
||||
// Build absolute URL
|
||||
$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;
|
||||
|
||||
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,
|
||||
]);
|
||||
|
||||
$answer['success'] = 1;
|
||||
$answer['message'] = 'Account created! Please check your email to verify your account.';
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log('[register] ' . $e->getMessage());
|
||||
$answer['message'] = 'Registration failed. Please try again.';
|
||||
http_response_code(500);
|
||||
// ── 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));
|
||||
}
|
||||
|
||||
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));
|
||||
Reference in New Issue
Block a user