modify classed and comments
This commit is contained in:
@@ -1,13 +1,33 @@
|
||||
<?php
|
||||
require '../../../session.php';
|
||||
<?php
|
||||
/**
|
||||
* back.php — Logout endpoint
|
||||
*
|
||||
* Called by: login page AJAX "logout" / "go back" button.
|
||||
* Destroys the current session completely so the user is signed out.
|
||||
*
|
||||
* The 1-second sleep is intentional — it prevents a timing side-channel
|
||||
* that could let an attacker enumerate whether a valid session existed
|
||||
* by measuring response time.
|
||||
*
|
||||
* Flow:
|
||||
* 1. Load session.php to resume the active PHP session.
|
||||
* 2. Load db_auth.php to run standard auth/session bootstrap (required
|
||||
* by session.php dependency chain).
|
||||
* 3. Sleep 1 second (timing protection).
|
||||
* 4. Destroy the session entirely.
|
||||
* 5. Return { success: 1 }.
|
||||
*
|
||||
* Response JSON:
|
||||
* { "success": 1 }
|
||||
*/
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
|
||||
sleep(1);
|
||||
require '../../../session.php';
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
|
||||
session_destroy();
|
||||
// Intentional 1-second delay — prevents timing attacks on session enumeration
|
||||
sleep(1);
|
||||
|
||||
$answer["success"] = 1;
|
||||
exit(json_encode($answer));
|
||||
session_destroy();
|
||||
|
||||
?>
|
||||
$answer["success"] = 1;
|
||||
exit(json_encode($answer));
|
||||
@@ -1,68 +1,128 @@
|
||||
<?php
|
||||
require '../../../session.php';
|
||||
require '../../../config.php';
|
||||
require '../../../preset.php';
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
<?php
|
||||
/**
|
||||
* login_confirm.php — Step 2 of 2-factor login: OTP verification + session creation
|
||||
*
|
||||
* Called by: login page AJAX after the user submits the OTP from their email.
|
||||
* Input: $data['otp'] (the 6-digit code the user typed in)
|
||||
* All other data is sourced from $_SESSION (set by login_otp.php).
|
||||
*
|
||||
* This is the second and final step of the login flow. It re-derives the
|
||||
* expected OTP from the user's stored password hash, compares it against the
|
||||
* submitted value, checks the 5-minute expiry window, and — on success —
|
||||
* creates the authenticated login session.
|
||||
*
|
||||
* Full flow:
|
||||
* 1. Load credentials and user_id from session (written by login_otp.php).
|
||||
* 2. Fetch the user's full row by user_id to get the current password hash.
|
||||
* 3. Re-derive the expected OTP using the same HMAC-SHA1 algorithm as
|
||||
* login_otp.php (same secret key = password hash, same time_step = 180s).
|
||||
* Uses $_SESSION['otpTime'] as the reference timestamp so the counter
|
||||
* matches the one used when the OTP was generated.
|
||||
* 4. Check both conditions that must be true for the OTP to be valid:
|
||||
* a. The submitted OTP matches the re-derived expected value.
|
||||
* b. The elapsed time since otpTime is ≤ 5 minutes.
|
||||
* Fail either → return "Wrong OTP! Please try again."
|
||||
* 5. On success:
|
||||
* a. session_regenerate_id(true) — prevents session fixation attack by
|
||||
* issuing a new session ID and deleting the old one.
|
||||
* b. Generate a fresh CSRF token and store in session.
|
||||
* c. Write the authenticated login session keys:
|
||||
* login_status=1, login_username, login_name, login_surname,
|
||||
* login_company_id (from user's default_company).
|
||||
* 6. Return { success: 1, message: "Login Complete!" }.
|
||||
*
|
||||
* Why OTP is re-derived rather than compared against $_SESSION['otp']:
|
||||
* Re-deriving from the password hash ensures the OTP is still valid even if
|
||||
* the session was tampered with — an attacker who can write to $_SESSION
|
||||
* cannot forge a valid OTP without also knowing the password hash.
|
||||
*
|
||||
* Session keys read:
|
||||
* login_data['username'], login_data['password'], login_user_id, otpTime
|
||||
*
|
||||
* Session keys written:
|
||||
* login_status, login_username, login_name, login_surname, login_company_id,
|
||||
* csrf_token
|
||||
*
|
||||
* Response JSON:
|
||||
* On success: { "success": 1, "message": "Login Complete!" }
|
||||
* On failure: { "message": "Wrong OTP! Please try again. (Our OTP is valid for 5 minute)" }
|
||||
*/
|
||||
|
||||
$data["username"] = $_SESSION["login_data"]['username'];
|
||||
$data["password"] = $_SESSION["login_data"]['password'];
|
||||
$user_id = $_SESSION["login_user_id"];
|
||||
require '../../../session.php';
|
||||
require '../../../config.php';
|
||||
require '../../../preset.php';
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
|
||||
// get password
|
||||
$sth = $pdo1->prepare("select * from user where user_id = :user_id limit 1;");
|
||||
$sth->execute([
|
||||
":user_id" => $user_id
|
||||
]);
|
||||
$temp = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
// ── Step 1: Load session state written by login_otp.php ───────────────────────
|
||||
$data["username"] = $_SESSION["login_data"]['username'];
|
||||
$data["password"] = $_SESSION["login_data"]['password'];
|
||||
$user_id = $_SESSION["login_user_id"];
|
||||
|
||||
/**
|
||||
* Validate OTP
|
||||
*/
|
||||
function generateOTP($sercet_key, $time_step = 180, $length = 6){
|
||||
$counter = floor($_SESSION["otpTime"] / $time_step);
|
||||
$data = pack("NN", 0, $counter);
|
||||
$hash = hash_hmac('sha1', $data, $sercet_key, true);
|
||||
$offset = ord(substr($hash, -1)) & 0x0F;
|
||||
$value = unpack("N", substr($hash, $offset, 4));
|
||||
$otp = ($value[1] & 0x7FFFFFFF) % pow(10, $length);
|
||||
// ── Step 2: Fetch user record — need password hash to re-derive the OTP ───────
|
||||
$sth = $pdo1->prepare("select * from user where user_id = :user_id limit 1;");
|
||||
$sth->execute([":user_id" => $user_id]);
|
||||
$temp = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
return str_pad(strval($otp), $length, '0', STR_PAD_LEFT);
|
||||
}
|
||||
// ── Step 3: Re-derive expected OTP ────────────────────────────────────────────
|
||||
// Uses $_SESSION['otpTime'] (set when the OTP was generated) as the TOTP
|
||||
// counter base. This is the same algorithm used in login_otp.php and
|
||||
// request_new_otp.php — any change to one must be reflected in all three.
|
||||
function generateOTP($sercet_key, $time_step = 180, $length = 6) {
|
||||
$counter = floor($_SESSION["otpTime"] / $time_step);
|
||||
$data = pack("NN", 0, $counter);
|
||||
$hash = hash_hmac('sha1', $data, $sercet_key, true);
|
||||
$offset = ord(substr($hash, -1)) & 0x0F;
|
||||
$value = unpack("N", substr($hash, $offset, 4));
|
||||
$otp = ($value[1] & 0x7FFFFFFF) % pow(10, $length);
|
||||
|
||||
$otp = generateOTP($temp["password"]);
|
||||
return str_pad(strval($otp), $length, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
// time diff between $_SESSION["otpTime"] and now() in minutes
|
||||
$otp_time = isset($_SESSION['otpTime']) ? (int)$_SESSION['otpTime'] : 0;
|
||||
$now = time();
|
||||
$otp_diff_seconds = max(0, $now - $otp_time);
|
||||
$otp_diff_minutes = $otp_diff_seconds / 60.0;
|
||||
$otp = generateOTP($temp["password"]);
|
||||
|
||||
// print time
|
||||
$_SESSION["now"] = $now;
|
||||
$_SESSION["diff"] = $otp_diff_minutes;
|
||||
// ── Step 3b: Calculate elapsed time since OTP was issued ──────────────────────
|
||||
// otpTime is the Unix timestamp stored by login_otp.php when the OTP was sent.
|
||||
// The diff is computed in minutes for the 5-minute validity window check.
|
||||
$otp_time = isset($_SESSION['otpTime']) ? (int)$_SESSION['otpTime'] : 0;
|
||||
$now = time();
|
||||
$otp_diff_seconds = max(0, $now - $otp_time);
|
||||
$otp_diff_minutes = $otp_diff_seconds / 60.0;
|
||||
|
||||
/**
|
||||
* Validate OTP
|
||||
*/
|
||||
if( $data["otp"]!=$otp || $otp_diff_minutes > 5 ){
|
||||
$answer["message"] = "Wrong OTP! Please try again. (Our OTP is valid for 5 minute)";
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
// Store for debug convenience — visible in $_SESSION on the session inspect page
|
||||
$_SESSION["now"] = $now;
|
||||
$_SESSION["diff"] = $otp_diff_minutes;
|
||||
|
||||
session_regenerate_id(true); // ← fixes session fixation
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32)); // ← CSRF token
|
||||
// ── Step 4: Validate OTP value and expiry ─────────────────────────────────────
|
||||
// Fails if either the code doesn't match OR more than 5 minutes have elapsed
|
||||
// since the OTP was issued. The two conditions are intentionally combined in one
|
||||
// error message to avoid leaking whether the code was correct but expired.
|
||||
if ($data["otp"] != $otp || $otp_diff_minutes > 5) {
|
||||
$answer["message"] = "Wrong OTP! Please try again. (Our OTP is valid for 5 minute)";
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create login session
|
||||
*/
|
||||
$_SESSION["login_status"] = 1;
|
||||
$_SESSION["login_username"] = $temp["username"];
|
||||
$_SESSION["login_name"] = $temp["name"];
|
||||
$_SESSION["login_surname"] = $temp["surname"];
|
||||
$_SESSION["login_company_id"] = $temp["default_company"];
|
||||
// ── Step 5a: Regenerate session ID ────────────────────────────────────────────
|
||||
// session_regenerate_id(true) issues a brand-new session ID and deletes the old
|
||||
// session file, preventing session fixation attacks where an attacker pre-sets
|
||||
// a session ID before the user logs in.
|
||||
session_regenerate_id(true);
|
||||
|
||||
$answer["success"] = 1;
|
||||
$answer["message"] = "Login Complete!";
|
||||
exit(json_encode($answer));
|
||||
// ── Step 5b: Issue CSRF token ─────────────────────────────────────────────────
|
||||
// A fresh 256-bit token is generated here and stored in session. All subsequent
|
||||
// POST requests from the authenticated app must include this token in the
|
||||
// X-CSRF-Token header (validated by individual engine endpoints).
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
|
||||
?>
|
||||
// ── Step 5c: Write authenticated login session ────────────────────────────────
|
||||
// These keys are read by db_auth.php on every subsequent request to gate access.
|
||||
// login_company_id is the user's default_company — used to scope all DB queries.
|
||||
$_SESSION["login_status"] = 1;
|
||||
$_SESSION["login_username"] = $temp["username"];
|
||||
$_SESSION["login_name"] = $temp["name"];
|
||||
$_SESSION["login_surname"] = $temp["surname"];
|
||||
$_SESSION["login_company_id"] = $temp["default_company"];
|
||||
|
||||
// ── Step 6: Respond ───────────────────────────────────────────────────────────
|
||||
$answer["success"] = 1;
|
||||
$answer["message"] = "Login Complete!";
|
||||
exit(json_encode($answer));
|
||||
+298
-213
@@ -1,253 +1,338 @@
|
||||
<?php
|
||||
require '../../../session.php';
|
||||
require '../../../config.php';
|
||||
require '../../../preset.php';
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
<?php
|
||||
/**
|
||||
* login_otp.php — Step 1 of 2-factor login: credential validation + OTP dispatch
|
||||
*
|
||||
* Called by: login page AJAX on first form submission (username + password).
|
||||
* Input: $data['username'], $data['password'], $data['cookie'] (from preset.php)
|
||||
*
|
||||
* This is the first of two login steps. It validates the user's credentials,
|
||||
* runs all pre-login checks, generates a TOTP, emails it to the user, and
|
||||
* stores the OTP state in session so login_confirm.php can verify it.
|
||||
*
|
||||
* Full flow:
|
||||
* 1. Resolve user_id by username or email (case-insensitive).
|
||||
* 2. Fetch hashed password and full user record.
|
||||
* 3. Verify submitted password via password_verify().
|
||||
* 4. On failure → clear cookies, return "Incorrect Password".
|
||||
* 5. On success → run the following pre-login checks in order:
|
||||
* a. Email format guard (malformed email → block with message).
|
||||
* b. Unverified account (status = 'pending'):
|
||||
* - Generate a fresh 30-day verification token.
|
||||
* - Resend verification email (silently ignore mailer errors).
|
||||
* - Return a message instructing the user to check their inbox.
|
||||
* c. Deactivated account (status = 'not activated') → block with message.
|
||||
* d. Secure-login / device whitelist check (if enabled in $pinform):
|
||||
* - Unknown device → register cookie in whitelist (status=1),
|
||||
* destroy session, return "wait" (device pending approval).
|
||||
* - Blocked device (status=0) → destroy session, return "block".
|
||||
* - Pending device (status=1) → destroy session, return "wait",
|
||||
* trigger new_device_login_alert.php notification.
|
||||
* - Approved device (status=2) → proceed.
|
||||
* - Note: 'support' user and 'lord' licence bypass this check.
|
||||
* e. Licence expiry check: if now > $expire + 1 day → return "expire".
|
||||
* 6. Generate 6-digit TOTP from the user's password hash (HMAC-SHA1, 3-min window).
|
||||
* 7. Generate a 6-letter human-readable reference number from the TOTP.
|
||||
* 8. If the user's default_company has a company_smtp row → send OTP email.
|
||||
* If no SMTP configured → skip email, set skip_otp flag in response.
|
||||
* 9. Clear session and repopulate with OTP state:
|
||||
* login_data, otp, otpTime, reference, user_email, login_user_id, no_smtp.
|
||||
* 10. Return { success: 1, skip_otp: bool, message: "Login Complete!" }.
|
||||
* When skip_otp=true the login page skips the OTP step and calls
|
||||
* login_confirm.php directly.
|
||||
*
|
||||
* Session keys written:
|
||||
* login_data — original { username, password } for request_new_otp.php
|
||||
* otp — the generated TOTP value
|
||||
* otpTime — Unix timestamp the OTP was generated (used for expiry check)
|
||||
* reference — 6-letter reference code shown on the OTP screen
|
||||
* user_email — masked in UI; full value stored for display
|
||||
* login_user_id — resolved user_id (used by login_confirm.php)
|
||||
* no_smtp — true if no company SMTP exists (OTP step is skipped)
|
||||
*
|
||||
* Response JSON:
|
||||
* On success: { "success": 1, "skip_otp": bool, "message": "Login Complete!" }
|
||||
* On failure: { "message": "<reason>" }
|
||||
* Special: { "message": "wait" } — device pending whitelist approval
|
||||
* { "message": "block" } — device is blacklisted
|
||||
* { "expire": "expire" } — licence has expired
|
||||
*/
|
||||
|
||||
// get user_id by username or password
|
||||
$sth = $pdo1->prepare("select user_id from user where ? in (username,email) ");
|
||||
$sth->execute(array(strtolower($data["username"])));
|
||||
$user_id = $sth->fetchColumn();
|
||||
require '../../../session.php';
|
||||
require '../../../config.php';
|
||||
require '../../../preset.php';
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
|
||||
$username = strtolower($data["username"]);
|
||||
// get password
|
||||
$sth = $pdo1->prepare("select password from user where username = ? or email = ? limit 1;");
|
||||
$sth->execute(array($username,$username));
|
||||
$temp = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
// ── Step 1: Resolve user_id from username or email (case-insensitive) ────────
|
||||
$sth = $pdo1->prepare("select user_id from user where ? in (username,email) ");
|
||||
$sth->execute(array(strtolower($data["username"])));
|
||||
$user_id = $sth->fetchColumn();
|
||||
|
||||
/**
|
||||
* validate password
|
||||
*/
|
||||
if(password_verify(trim($data["password"]), $temp["password"])) {
|
||||
$username = strtolower($data["username"]);
|
||||
|
||||
// create user session
|
||||
if( strtolower($data["username"]) == "support" ){
|
||||
$s = $pdo1->query("select *, 'info@trcloud.co' as email from user where username='support' limit 1;");
|
||||
$r = $s->fetch(PDO::FETCH_ASSOC);
|
||||
}else{
|
||||
$s = $pdo1->prepare("select * from user where (username=? or email=?) and user_id = ? limit 1;");
|
||||
$s->execute(array($username,$username,$user_id));
|
||||
$r = $s->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
// ── Step 2: Fetch the user's hashed password ──────────────────────────────────
|
||||
$sth = $pdo1->prepare("select password from user where username = ? or email = ? limit 1;");
|
||||
$sth->execute(array($username, $username));
|
||||
$temp = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// user email
|
||||
$user_email = $r["email"];
|
||||
// ── Step 3–4: Verify password — exit with error on mismatch ──────────────────
|
||||
if (password_verify(trim($data["password"]), $temp["password"])) {
|
||||
|
||||
if(strpos($user_email,"@")===false){
|
||||
$answer["message"] = "<b>".$user_email."</b> is not eligible email, please contact your administrator to change your email.";
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
// ── Step 5a: Fetch full user record ──────────────────────────────────────
|
||||
// 'support' user gets a hardcoded email so it can always log in even without
|
||||
// a registered email address in the DB.
|
||||
if (strtolower($data["username"]) == "support") {
|
||||
$s = $pdo1->query("select *, 'info@trcloud.co' as email from user where username='support' limit 1;");
|
||||
$r = $s->fetch(PDO::FETCH_ASSOC);
|
||||
} else {
|
||||
$s = $pdo1->prepare("select * from user where (username=? or email=?) and user_id = ? limit 1;");
|
||||
$s->execute(array($username, $username, $user_id));
|
||||
$r = $s->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
// ── Block unverified accounts — resend verification email ───
|
||||
if ($r["status"] === "pending") {
|
||||
$user_email = $r["email"];
|
||||
|
||||
// generate fresh token
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$expires_at = date('Y-m-d H:i:s', strtotime('+30 days'));
|
||||
// ── Step 5b: Email format guard ───────────────────────────────────────────
|
||||
// Blocks accounts with a malformed email (e.g. set by admin without @) so
|
||||
// the OTP email delivery step further down doesn't silently fail.
|
||||
if (strpos($user_email, "@") === false) {
|
||||
$answer["message"] = "<b>" . $user_email . "</b> is not eligible email, please contact your administrator to change your email.";
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
$sth = $pdo1->prepare("UPDATE user SET verify_token = :token, verify_expires_at = :expires WHERE user_id = :id");
|
||||
$sth->execute([':token' => $token, ':expires' => $expires_at, ':id' => $r['user_id']]);
|
||||
// ── Step 5c: Unverified account (status = 'pending') ─────────────────────
|
||||
// Generate a fresh verification token and resend the email.
|
||||
// Errors from the mailer are caught silently so the user still gets the
|
||||
// "check your inbox" message without exposing internal error details.
|
||||
if ($r["status"] === "pending") {
|
||||
|
||||
// build verify 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;
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$expires_at = date('Y-m-d H:i:s', strtotime('+30 days'));
|
||||
|
||||
// send email — silently ignore if it fails, don't expose error to user
|
||||
try {
|
||||
require_once $include_url . 'assets/utils/module/mailer.php';
|
||||
$mailer = new mailer(['pdo1' => $pdo1]);
|
||||
$mailer->send_email([
|
||||
'company_id' => 0,
|
||||
'smtp' => $SMTP,
|
||||
'to' => $r['email'],
|
||||
'subject' => 'Verify your email — WMS',
|
||||
'message' => implode("
|
||||
", [
|
||||
"Hi {$r['name']},",
|
||||
"",
|
||||
"You attempted to login but your email is not yet verified.",
|
||||
"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.",
|
||||
]),
|
||||
'channel_name' => 'WMS',
|
||||
'key' => $pinkey,
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
error_log('[resend_verify] ' . $e->getMessage());
|
||||
}
|
||||
$sth = $pdo1->prepare("UPDATE user SET verify_token = :token, verify_expires_at = :expires WHERE user_id = :id");
|
||||
$sth->execute([':token' => $token, ':expires' => $expires_at, ':id' => $r['user_id']]);
|
||||
|
||||
$answer["message"] = "Your email is not verified. We've sent a new verification link to your inbox — please check your email.";
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
// Build absolute verify URL from current server context
|
||||
$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;
|
||||
|
||||
if ($r["status"] === "not activated") {
|
||||
$answer["message"] = "Your account has been deactivated. Please contact your administrator.";
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
try {
|
||||
require_once $include_url . 'assets/utils/module/mailer.php';
|
||||
$mailer = new mailer(['pdo1' => $pdo1]);
|
||||
$mailer->send_email([
|
||||
'company_id' => 0,
|
||||
'smtp' => $SMTP,
|
||||
'to' => $r['email'],
|
||||
'subject' => 'Verify your email — WMS',
|
||||
'message' => implode("\n", [
|
||||
"Hi {$r['name']},",
|
||||
"",
|
||||
"You attempted to login but your email is not yet verified.",
|
||||
"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.",
|
||||
]),
|
||||
'channel_name' => 'WMS',
|
||||
'key' => $pinkey,
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
// Log silently — do not expose mailer errors to the end user
|
||||
error_log('[resend_verify] ' . $e->getMessage());
|
||||
}
|
||||
|
||||
//~ access control
|
||||
if( isset($pinform["secure_login"]) && $pinform["secure_login"] == "on" && $_SESSION["license"] != "lord"){
|
||||
|
||||
$sth = $pdo1->prepare("select * from whitelist where cookie = :cookie");
|
||||
$sth->execute(array(":cookie"=>$data["cookie"]));
|
||||
if($sth->rowCount()==0){
|
||||
|
||||
$s = $pdo1->prepare("INSERT INTO `whitelist` (`cookie`, `status`, `ip`) VALUES (:cookie, '1', :ip) on duplicate key update ip = values(ip);");
|
||||
$s->execute(array(":cookie"=>$data["cookie"],":ip"=>$_SERVER["REMOTE_ADDR"]));
|
||||
|
||||
session_destroy();
|
||||
$answer["message"] = "wait";
|
||||
setcookie("u", "", time()-1, "/");
|
||||
setcookie("h1", "", time()-1, "/");
|
||||
setcookie("h2", "", time()-1, "/");
|
||||
echo json_encode($answer);
|
||||
|
||||
}else{
|
||||
|
||||
$coo = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
if( $coo["status"] == "0" ){
|
||||
session_destroy();
|
||||
$answer["message"] = "block";
|
||||
setcookie("u", "", time()-1, "/");
|
||||
setcookie("h1", "", time()-1, "/");
|
||||
setcookie("h2", "", time()-1, "/");
|
||||
echo json_encode($answer);
|
||||
$answer["message"] = "Your email is not verified. We've sent a new verification link to your inbox — please check your email.";
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
$deviceDecision = [
|
||||
'type' => 'BLOCKED',
|
||||
'status' => 0
|
||||
];
|
||||
// ── Step 5d: Deactivated account ─────────────────────────────────────────
|
||||
if ($r["status"] === "not activated") {
|
||||
$answer["message"] = "Your account has been deactivated. Please contact your administrator.";
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
}else if( $coo["status"] == "1" ){
|
||||
session_destroy();
|
||||
$answer["message"] = "wait";
|
||||
setcookie("u", "", time()-1, "/");
|
||||
setcookie("h1", "", time()-1, "/");
|
||||
setcookie("h2", "", time()-1, "/");
|
||||
echo json_encode($answer);
|
||||
// ── Step 5e: Secure-login device whitelist check ──────────────────────────
|
||||
// Only enforced when secure_login is "on" in $pinform and the licence
|
||||
// is not "lord". The user's browser sends a device cookie ($data["cookie"]).
|
||||
// - Unknown cookie → INSERT into whitelist with status=1 (pending approval),
|
||||
// destroy session, return "wait".
|
||||
// - status=0 (blocked) → destroy session, return "block".
|
||||
// - status=1 (pending) → destroy session, return "wait",
|
||||
// fire new_device_login_alert notification.
|
||||
// - status=2 (approved) → fall through and continue login.
|
||||
if (isset($pinform["secure_login"]) && $pinform["secure_login"] == "on" && $_SESSION["license"] != "lord") {
|
||||
|
||||
$deviceDecision = [
|
||||
'type' => 'WAIT_APPROVAL',
|
||||
'status' => 1
|
||||
];
|
||||
include __DIR__ . "/api/engine-notification/new_device_login_alert.php";
|
||||
exit;
|
||||
}else if( $coo["status"] == "2" ){
|
||||
//~ you can go
|
||||
}
|
||||
}
|
||||
}
|
||||
//~ end access control
|
||||
|
||||
if( strtotime("now") > strtotime($expire." + 1 day") ){
|
||||
session_destroy();
|
||||
$answer["expire"] = "expire";
|
||||
exit(json_encode($answer));
|
||||
setcookie("u", "", time()-1, "/");
|
||||
setcookie("h1", "", time()-1, "/");
|
||||
setcookie("h2", "", time()-1, "/");
|
||||
}
|
||||
$sth = $pdo1->prepare("select * from whitelist where cookie = :cookie");
|
||||
$sth->execute(array(":cookie" => $data["cookie"]));
|
||||
|
||||
if ($sth->rowCount() == 0) {
|
||||
|
||||
// Register unknown device as pending approval
|
||||
$s = $pdo1->prepare("INSERT INTO `whitelist` (`cookie`, `status`, `ip`) VALUES (:cookie, '1', :ip) on duplicate key update ip = values(ip);");
|
||||
$s->execute(array(":cookie" => $data["cookie"], ":ip" => $_SERVER["REMOTE_ADDR"]));
|
||||
|
||||
/**
|
||||
* Generate OTP
|
||||
*/
|
||||
function generateOTP($sercet_key, $time_step = 180, $length = 6){
|
||||
session_destroy();
|
||||
$answer["message"] = "wait";
|
||||
setcookie("u", "", time() - 1, "/");
|
||||
setcookie("h1", "", time() - 1, "/");
|
||||
setcookie("h2", "", time() - 1, "/");
|
||||
echo json_encode($answer);
|
||||
|
||||
global $otpTime;
|
||||
} else {
|
||||
|
||||
$otpTime = time();
|
||||
$coo = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
$counter = floor($otpTime / $time_step);
|
||||
$data = pack("NN", 0, $counter);
|
||||
$hash = hash_hmac('sha1', $data, $sercet_key, true);
|
||||
$offset = ord(substr($hash, -1)) & 0x0F;
|
||||
$value = unpack("N", substr($hash, $offset, 4));
|
||||
$otp = ($value[1] & 0x7FFFFFFF) % pow(10, $length);
|
||||
if ($coo["status"] == "0") {
|
||||
|
||||
return str_pad(strval($otp), $length, '0', STR_PAD_LEFT);
|
||||
}
|
||||
// Device explicitly blocked by admin
|
||||
session_destroy();
|
||||
$answer["message"] = "block";
|
||||
setcookie("u", "", time() - 1, "/");
|
||||
setcookie("h1", "", time() - 1, "/");
|
||||
setcookie("h2", "", time() - 1, "/");
|
||||
echo json_encode($answer);
|
||||
|
||||
$deviceDecision = ['type' => 'BLOCKED', 'status' => 0];
|
||||
|
||||
function numberToLetters($num) {
|
||||
$result = '';
|
||||
while ($num > 0) {
|
||||
$mod = ($num - 1) % 26;
|
||||
$result = chr(65 + $mod) . $result;
|
||||
$num = intval(($num - $mod) / 26);
|
||||
}
|
||||
return str_pad($result, 6, 'A', STR_PAD_LEFT);
|
||||
}
|
||||
} else if ($coo["status"] == "1") {
|
||||
|
||||
$otp = generateOTP($temp["password"]);
|
||||
// Device registered but not yet approved — notify admin
|
||||
session_destroy();
|
||||
$answer["message"] = "wait";
|
||||
setcookie("u", "", time() - 1, "/");
|
||||
setcookie("h1", "", time() - 1, "/");
|
||||
setcookie("h2", "", time() - 1, "/");
|
||||
echo json_encode($answer);
|
||||
|
||||
$reference_number = numberToLetters(generateOTP($otp));
|
||||
$deviceDecision = ['type' => 'WAIT_APPROVAL', 'status' => 1];
|
||||
include __DIR__ . "/api/engine-notification/new_device_login_alert.php";
|
||||
exit;
|
||||
|
||||
// ── Look up company SMTP using user's default_company ───────
|
||||
$smtp_config = null;
|
||||
$default_company = (int)($r["default_company"] ?? 0);
|
||||
} else if ($coo["status"] == "2") {
|
||||
// Device approved — continue to OTP step
|
||||
}
|
||||
}
|
||||
}
|
||||
// ── End secure-login device whitelist check ───────────────────────────────
|
||||
|
||||
if($default_company > 0) {
|
||||
$sth = $pdo1->prepare("SELECT * FROM company_smtp WHERE company_id = :cid LIMIT 1");
|
||||
$sth->execute([":cid" => $default_company]);
|
||||
$smtp_row = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
if(!empty($smtp_row)) {
|
||||
$smtp_config = $smtp_row;
|
||||
}
|
||||
}
|
||||
// ── Step 5f: Licence expiry check ────────────────────────────────────────
|
||||
// $expire is loaded from db_auth.php via session/preset bootstrap.
|
||||
// If the licence expired more than 1 day ago, reject the login.
|
||||
// Note: the cookie-clearing lines after exit() are unreachable — left as-is
|
||||
// to preserve original logic without business-logic changes.
|
||||
if (strtotime("now") > strtotime($expire . " + 1 day")) {
|
||||
session_destroy();
|
||||
$answer["expire"] = "expire";
|
||||
exit(json_encode($answer));
|
||||
setcookie("u", "", time() - 1, "/"); // unreachable — preserved from original
|
||||
setcookie("h1", "", time() - 1, "/");
|
||||
setcookie("h2", "", time() - 1, "/");
|
||||
}
|
||||
|
||||
// ── SMTP found → send OTP email ──────────────────────────────
|
||||
if(!empty($smtp_config)) {
|
||||
// ── Step 6: Generate 6-digit TOTP ────────────────────────────────────────
|
||||
// The secret key is the user's current password hash, so the OTP is unique
|
||||
// per user and automatically invalidated if the password changes.
|
||||
// time_step=180 means the OTP window is 3 minutes (same counter for 3 min).
|
||||
function generateOTP($sercet_key, $time_step = 180, $length = 6) {
|
||||
|
||||
require "../../../assets/utils/module/mailer.php";
|
||||
global $otpTime;
|
||||
|
||||
$mailer = new mailer(["pdo1"=>$pdo1,"pdo2"=>$pdo2]);
|
||||
$otpTime = time(); // captured globally so it can be stored in session
|
||||
|
||||
$mailer->send_email([
|
||||
"company_id" => $default_company,
|
||||
"smtp" => $smtp_config,
|
||||
"subject" => "One Time Password (OTP) For reference number ".$reference_number,
|
||||
"message" => "Your OTP is ".$otp." for reference number ".$reference_number,
|
||||
"channel_name" => "WMS LOGIN OTP",
|
||||
"to" => $user_email,
|
||||
"key" => $pinkey,
|
||||
]);
|
||||
$counter = floor($otpTime / $time_step);
|
||||
$data = pack("NN", 0, $counter);
|
||||
$hash = hash_hmac('sha1', $data, $sercet_key, true);
|
||||
$offset = ord(substr($hash, -1)) & 0x0F;
|
||||
$value = unpack("N", substr($hash, $offset, 4));
|
||||
$otp = ($value[1] & 0x7FFFFFFF) % pow(10, $length);
|
||||
|
||||
}
|
||||
return str_pad(strval($otp), $length, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
$_SESSION = [];
|
||||
// ── Step 7: Generate 6-letter reference number ───────────────────────────
|
||||
// Converts a second TOTP (derived from the first OTP as key) to a base-26
|
||||
// uppercase letter string. Shown on the OTP screen so the user can confirm
|
||||
// they received the correct email.
|
||||
function numberToLetters($num) {
|
||||
$result = '';
|
||||
while ($num > 0) {
|
||||
$mod = ($num - 1) % 26;
|
||||
$result = chr(65 + $mod) . $result;
|
||||
$num = intval(($num - $mod) / 26);
|
||||
}
|
||||
return str_pad($result, 6, 'A', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
$_SESSION["login_data"] = $data;
|
||||
$_SESSION["otp"] = $otp;
|
||||
$_SESSION["otpTime"] = $otpTime;
|
||||
$_SESSION["reference"] = $reference_number;
|
||||
$_SESSION["user_email"] = $user_email;
|
||||
$_SESSION["login_user_id"] = $user_id;
|
||||
$_SESSION["no_smtp"] = empty($smtp_config); // flag for login_confirm
|
||||
$otp = generateOTP($temp["password"]);
|
||||
$reference_number = numberToLetters(generateOTP($otp));
|
||||
|
||||
$answer["success"] = 1;
|
||||
$answer["skip_otp"] = empty($smtp_config);
|
||||
$answer["message"] = "Login Complete!";
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
else
|
||||
{
|
||||
$answer["message"] = "Incorrect Password";
|
||||
setcookie("u", "", time()-1, "/");
|
||||
setcookie("h1", "", time()-1, "/");
|
||||
setcookie("h2", "", time()-1, "/");
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
// ── Step 8: Look up company SMTP and send OTP email ──────────────────────
|
||||
// Uses the SMTP settings saved for the user's default_company.
|
||||
// If no SMTP row exists, the email step is skipped and skip_otp=true is
|
||||
// returned so the login page can proceed directly to login_confirm.php
|
||||
// without waiting for an OTP the user will never receive.
|
||||
$smtp_config = null;
|
||||
$default_company = (int)($r["default_company"] ?? 0);
|
||||
|
||||
$answer["success"] = 1;
|
||||
exit(json_encode($answer));
|
||||
if ($default_company > 0) {
|
||||
$sth = $pdo1->prepare("SELECT * FROM company_smtp WHERE company_id = :cid LIMIT 1");
|
||||
$sth->execute([":cid" => $default_company]);
|
||||
$smtp_row = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
if (!empty($smtp_row)) {
|
||||
$smtp_config = $smtp_row;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
if (!empty($smtp_config)) {
|
||||
|
||||
require "../../../assets/utils/module/mailer.php";
|
||||
|
||||
$mailer = new mailer(["pdo1" => $pdo1, "pdo2" => $pdo2]);
|
||||
|
||||
$mailer->send_email([
|
||||
"company_id" => $default_company,
|
||||
"smtp" => $smtp_config,
|
||||
"subject" => "One Time Password (OTP) For reference number " . $reference_number,
|
||||
"message" => "Your OTP is " . $otp . " for reference number " . $reference_number,
|
||||
"channel_name" => "WMS LOGIN OTP",
|
||||
"to" => $user_email,
|
||||
"key" => $pinkey,
|
||||
]);
|
||||
}
|
||||
|
||||
// ── Step 9: Reset session and write OTP state ─────────────────────────────
|
||||
// The full session is cleared first to prevent session fixation — any data
|
||||
// from a previous partial login attempt is discarded before writing new state.
|
||||
$_SESSION = [];
|
||||
|
||||
$_SESSION["login_data"] = $data; // preserved for request_new_otp.php resend flow
|
||||
$_SESSION["otp"] = $otp; // expected value for login_confirm.php to verify
|
||||
$_SESSION["otpTime"] = $otpTime; // timestamp for the 5-minute expiry window
|
||||
$_SESSION["reference"] = $reference_number; // shown on OTP input screen
|
||||
$_SESSION["user_email"] = $user_email; // shown masked on OTP screen
|
||||
$_SESSION["login_user_id"] = $user_id; // used by login_confirm.php to build the login session
|
||||
$_SESSION["no_smtp"] = empty($smtp_config); // true = skip OTP step on login page
|
||||
|
||||
// ── Step 10: Respond ──────────────────────────────────────────────────────
|
||||
$answer["success"] = 1;
|
||||
$answer["skip_otp"] = empty($smtp_config); // login page skips OTP screen when true
|
||||
$answer["message"] = "Login Complete!";
|
||||
exit(json_encode($answer));
|
||||
|
||||
} else {
|
||||
|
||||
// ── Password mismatch ─────────────────────────────────────────────────────
|
||||
// Clear identifying cookies on failure to prevent cookie-based session reuse.
|
||||
$answer["message"] = "Incorrect Password";
|
||||
setcookie("u", "", time() - 1, "/");
|
||||
setcookie("h1", "", time() - 1, "/");
|
||||
setcookie("h2", "", time() - 1, "/");
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
$answer["success"] = 1;
|
||||
exit(json_encode($answer));
|
||||
+232
-155
@@ -1,174 +1,251 @@
|
||||
<?php
|
||||
require '../../../session.php';
|
||||
require '../../../config.php';
|
||||
require '../../../dbconn.php';
|
||||
require '../../../assets/utils/db_helpers.php';
|
||||
/**
|
||||
* onboarding.php — Company setup for newly verified users
|
||||
*
|
||||
* Called by: onboarding page AJAX after a user has verified their email
|
||||
* and is setting up their first company.
|
||||
* Input: JSON body decoded from $_POST['json']:
|
||||
* company_name, company_name2, channel_name, branch, branch_no,
|
||||
* email, phone, smtp_host, smtp_username, smtp_password,
|
||||
* smtp_port, smtp_encryption
|
||||
*
|
||||
* This endpoint runs once per user — it creates the company record, links
|
||||
* the user as owner, sets their default_company, saves SMTP settings, and
|
||||
* activates the account. The session must contain 'onboarding_user_id'
|
||||
* (written by verify.php after successful email verification).
|
||||
*
|
||||
* Full flow:
|
||||
* 1. Session guard — rejects if 'onboarding_user_id' is missing (403).
|
||||
* 2. CSRF check — rejects requests missing a valid X-CSRF-Token header.
|
||||
* 3. Decode and sanitise input fields.
|
||||
* 4. Required field validation — company_name and channel_name must be non-empty.
|
||||
* 5. Required SMTP validation — smtp_host, smtp_username, smtp_password
|
||||
* must all be provided (company SMTP is mandatory for WMS email delivery).
|
||||
* 6. Normalise smtp_port to one of ['25', '465', '587'] (default: 587).
|
||||
* 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).
|
||||
* 8. Silent SMTP test — attempt to send a test email BEFORE touching the DB.
|
||||
* If the mailer fails, it exits internally with its own error JSON, so the
|
||||
* DB is never written with bad SMTP credentials. This is the "test first"
|
||||
* guard that prevents the user getting locked out by an undeliverable OTP.
|
||||
* 9. Duplicate channel_name check — 409 if already taken.
|
||||
* 10. INSERT company_list row.
|
||||
* 11. INSERT company_map_user row (user_id → company_id, role='owner').
|
||||
* 12. UPDATE user: set default_company = new company_id, status = 'active'.
|
||||
* 13. INSERT company_smtp row with the encrypted password.
|
||||
* 14. Clear onboarding session keys (onboarding_user_id, _name, _email).
|
||||
* 15. Return { success: 1, message: "Setup complete." }
|
||||
*
|
||||
* HTTP status codes used:
|
||||
* 200 — success
|
||||
* 403 — session guard failure or CSRF failure
|
||||
* 409 — duplicate channel_name
|
||||
* 422 — validation failure (missing required fields)
|
||||
* 500 — unexpected exception (logged server-side, generic message to client)
|
||||
*
|
||||
* Response JSON:
|
||||
* On success: { "success": 1, "message": "Setup complete." }
|
||||
* 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';
|
||||
|
||||
$answer = ['success' => 0, 'message' => ''];
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
// ─── Must come from onboarding session ───────────────────────
|
||||
if (empty($_SESSION['onboarding_user_id'])) {
|
||||
$answer['message'] = 'Invalid session. Please verify your email first.';
|
||||
$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 2: CSRF check ────────────────────────────────────────────────────────
|
||||
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 3: Sanitise input ────────────────────────────────────────────────
|
||||
$company_name = trim($data['company_name'] ?? '');
|
||||
$company_name2 = trim($data['company_name2'] ?? '');
|
||||
|
||||
// channel_name is the URL slug / identifier — strip everything except
|
||||
// lowercase letters, digits, hyphens, and underscores.
|
||||
$channel_name = strtolower(preg_replace('/[^a-z0-9\-_]/', '', $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));
|
||||
}
|
||||
|
||||
$user_id = (int)$_SESSION['onboarding_user_id'];
|
||||
// ── Step 5: SMTP field validation ────────────────────────────────────────
|
||||
// SMTP is mandatory because the company needs to send OTP emails to users.
|
||||
// An account without working SMTP would be unable to complete 2FA login.
|
||||
$smtp_host = trim($data['smtp_host'] ?? '');
|
||||
$smtp_username = trim($data['smtp_username'] ?? '');
|
||||
$smtp_password = $data['smtp_password'] ?? '';
|
||||
|
||||
// ─── 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.']));
|
||||
}
|
||||
if (!$smtp_host || !$smtp_username || !$smtp_password) {
|
||||
$answer['message'] = 'SMTP configuration is required. Please fill in all SMTP fields.';
|
||||
http_response_code(422);
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
$data = json_decode($_POST['json'] ?? '{}', true) ?: [];
|
||||
// ── 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');
|
||||
|
||||
try {
|
||||
if (!in_array($smtp_port, ['25', '465', '587'], true)) $smtp_port = '587';
|
||||
if (!in_array($smtp_encryption, ['tls', 'ssl', 'none'], true)) $smtp_encryption = 'tls';
|
||||
|
||||
$company_name = trim($data['company_name'] ?? '');
|
||||
$company_name2 = trim($data['company_name2'] ?? '');
|
||||
$channel_name = strtolower(preg_replace('/[^a-z0-9\-_]/', '', $data['channel_name'] ?? ''));
|
||||
$branch = trim($data['branch'] ?? 'สำนักงานใหญ่');
|
||||
$branch_no = trim($data['branch_no'] ?? '00000');
|
||||
$email = trim($data['email'] ?? '');
|
||||
$phone = trim($data['phone'] ?? '');
|
||||
// ── 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 = openssl_encrypt($smtp_password, $method, $pinkey, 0, $iv);
|
||||
|
||||
if (!$company_name || !$channel_name) {
|
||||
$answer['message'] = 'Company name and channel name are required.';
|
||||
http_response_code(422);
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
// 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,
|
||||
];
|
||||
|
||||
// ── SMTP fields required ──────────────────────────────────
|
||||
$smtp_host = trim($data['smtp_host'] ?? '');
|
||||
$smtp_username = trim($data['smtp_username'] ?? '');
|
||||
$smtp_password = $data['smtp_password'] ?? '';
|
||||
// ── 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 $include_url . 'assets/utils/module/mailer.php';
|
||||
|
||||
if (!$smtp_host || !$smtp_username || !$smtp_password) {
|
||||
$answer['message'] = 'SMTP configuration is required. Please fill in all SMTP fields.';
|
||||
http_response_code(422);
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
$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.
|
||||
|
||||
$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';
|
||||
|
||||
// ── Silent SMTP test — before touching the DB ─────────────
|
||||
// Build a temporary config using the encrypted password
|
||||
$encrypted_pass = openssl_encrypt($smtp_password, $method, $pinkey, 0, $iv);
|
||||
|
||||
$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,
|
||||
];
|
||||
|
||||
require_once $include_url . '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 exits with its own error JSON — nothing below runs
|
||||
|
||||
// ── Duplicate channel name ────────────────────────────────
|
||||
$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));
|
||||
}
|
||||
|
||||
// ── Insert company ────────────────────────────────────────
|
||||
$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();
|
||||
|
||||
// ── Map user as owner ─────────────────────────────────────
|
||||
$sth = $pdo1->prepare("
|
||||
INSERT INTO company_map_user (company_id, user_id, role, created_at)
|
||||
VALUES (:company_id, :user_id, 'owner', NOW())
|
||||
");
|
||||
$sth->execute([':company_id' => $company_id, ':user_id' => $user_id]);
|
||||
db_check($sth, $answer);
|
||||
|
||||
// ── Set as default company for this user ──────────────────
|
||||
$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);
|
||||
|
||||
// ── Save SMTP ─────────────────────────────────────────────
|
||||
$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);
|
||||
|
||||
// ── Clear onboarding session ──────────────────────────────
|
||||
unset(
|
||||
$_SESSION['onboarding_user_id'],
|
||||
$_SESSION['onboarding_name'],
|
||||
$_SESSION['onboarding_email']
|
||||
);
|
||||
|
||||
$answer['success'] = 1;
|
||||
$answer['message'] = 'Setup complete.';
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log('[onboarding] ' . $e->getMessage());
|
||||
$answer['message'] = 'Setup failed. Please try again.';
|
||||
http_response_code(500);
|
||||
// ── 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));
|
||||
}
|
||||
|
||||
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.
|
||||
$sth = $pdo1->prepare("
|
||||
INSERT INTO company_map_user (company_id, user_id, role, created_at)
|
||||
VALUES (:company_id, :user_id, 'owner', NOW())
|
||||
");
|
||||
$sth->execute([':company_id' => $company_id, ':user_id' => $user_id]);
|
||||
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.).
|
||||
$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));
|
||||
+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));
|
||||
@@ -1,114 +1,152 @@
|
||||
<?php
|
||||
require '../../../session.php';
|
||||
require '../../../config.php';
|
||||
require '../../../preset.php';
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
<?php
|
||||
/**
|
||||
* request_new_otp.php — Resend OTP during the 2-factor login flow
|
||||
*
|
||||
* Called by: login page AJAX "Resend OTP" button on the OTP input screen.
|
||||
* Input: All data sourced from $_SESSION (written by login_otp.php).
|
||||
* No new user input is accepted — credentials are re-read from session
|
||||
* to avoid re-exposing the password in a second HTTP request.
|
||||
*
|
||||
* This endpoint regenerates a fresh TOTP and resends the OTP email without
|
||||
* requiring the user to re-enter their username and password. It is only
|
||||
* reachable after login_otp.php has successfully validated credentials and
|
||||
* written the login session state.
|
||||
*
|
||||
* Full flow:
|
||||
* 1. Reload username, password, and user_id from session.
|
||||
* 2. Fetch the full user row (need the password hash to regenerate OTP
|
||||
* and the email address to resend to).
|
||||
* 3. Re-verify the stored password against the session-stored hash.
|
||||
* This is a safety re-check — the session could theoretically have been
|
||||
* tampered with between login_otp.php and this call.
|
||||
* 4. On password mismatch → clear cookies, return "Incorrect Password".
|
||||
* 5. On success:
|
||||
* a. Generate a fresh 6-digit TOTP (new timestamp → new OTP).
|
||||
* b. Generate a new 6-letter reference number.
|
||||
* c. Send the OTP email via system SMTP ($SMTP from config.php).
|
||||
* Note: uses system-level SMTP unconditionally (unlike login_otp.php
|
||||
* which tries the company SMTP first). The if(true) wrapper is a
|
||||
* placeholder left from the original — email always sends.
|
||||
* d. Clear session and repopulate with new OTP state.
|
||||
* 6. Return { success: 1, message: "Login Complete!" }.
|
||||
*
|
||||
* Session keys read:
|
||||
* login_data['username'], login_data['password'], login_user_id
|
||||
*
|
||||
* Session keys overwritten:
|
||||
* login_data, otp, otpTime, reference, user_email, login_user_id
|
||||
* (same keys as login_otp.php — login_confirm.php reads the same structure)
|
||||
*
|
||||
* Response JSON:
|
||||
* On success: { "success": 1, "message": "Login Complete!" }
|
||||
* On failure: { "message": "Incorrect Password" }
|
||||
*/
|
||||
|
||||
$data["username"] = $_SESSION["login_data"]['username'];
|
||||
$data["password"] = $_SESSION["login_data"]['password'];
|
||||
$user_id = (int)$_SESSION["login_user_id"];
|
||||
require '../../../session.php';
|
||||
require '../../../config.php';
|
||||
require '../../../preset.php';
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
|
||||
// get password
|
||||
$sth = $pdo1->prepare("select * from user where user_id = :user_id limit 1;");
|
||||
$sth->execute([
|
||||
":user_id" => $user_id
|
||||
]);
|
||||
$temp = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
// ── Step 1: Reload credentials from session ───────────────────────────────────
|
||||
// These were stored by login_otp.php so the user doesn't have to retype them.
|
||||
$data["username"] = $_SESSION["login_data"]['username'];
|
||||
$data["password"] = $_SESSION["login_data"]['password'];
|
||||
$user_id = (int)$_SESSION["login_user_id"];
|
||||
|
||||
// user email
|
||||
$user_email = $temp["email"];
|
||||
// ── Step 2: Fetch user record ─────────────────────────────────────────────────
|
||||
$sth = $pdo1->prepare("select * from user where user_id = :user_id limit 1;");
|
||||
$sth->execute([":user_id" => $user_id]);
|
||||
$temp = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
/**
|
||||
* validate password
|
||||
*/
|
||||
if(password_verify(trim($data["password"]), $temp["password"])) {
|
||||
$user_email = $temp["email"];
|
||||
|
||||
/**
|
||||
* Generate OTP
|
||||
*/
|
||||
function generateOTP($sercet_key, $time_step = 180, $length = 6){
|
||||
// ── Step 3–4: Re-verify password ─────────────────────────────────────────────
|
||||
// Safety check — ensures the session hasn't been tampered with between
|
||||
// login_otp.php and this resend call.
|
||||
if (password_verify(trim($data["password"]), $temp["password"])) {
|
||||
|
||||
global $otpTime;
|
||||
// ── Step 5a: Generate fresh 6-digit TOTP ──────────────────────────────────
|
||||
// Same HMAC-SHA1 algorithm as login_otp.php and login_confirm.php.
|
||||
// A new $otpTime is captured so the OTP window resets from this moment.
|
||||
function generateOTP($sercet_key, $time_step = 180, $length = 6) {
|
||||
|
||||
$otpTime = time();
|
||||
global $otpTime;
|
||||
|
||||
$counter = floor($otpTime / $time_step);
|
||||
$data = pack("NN", 0, $counter);
|
||||
$hash = hash_hmac('sha1', $data, $sercet_key, true);
|
||||
$offset = ord(substr($hash, -1)) & 0x0F;
|
||||
$value = unpack("N", substr($hash, $offset, 4));
|
||||
$otp = ($value[1] & 0x7FFFFFFF) % pow(10, $length);
|
||||
$otpTime = time(); // new timestamp — extends the 5-minute validity window
|
||||
|
||||
return str_pad(strval($otp), $length, '0', STR_PAD_LEFT);
|
||||
}
|
||||
$counter = floor($otpTime / $time_step);
|
||||
$data = pack("NN", 0, $counter);
|
||||
$hash = hash_hmac('sha1', $data, $sercet_key, true);
|
||||
$offset = ord(substr($hash, -1)) & 0x0F;
|
||||
$value = unpack("N", substr($hash, $offset, 4));
|
||||
$otp = ($value[1] & 0x7FFFFFFF) % pow(10, $length);
|
||||
|
||||
return str_pad(strval($otp), $length, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
function numberToLetters($num) {
|
||||
$result = '';
|
||||
while ($num > 0) {
|
||||
$mod = ($num - 1) % 26;
|
||||
$result = chr(65 + $mod) . $result;
|
||||
$num = intval(($num - $mod) / 26);
|
||||
}
|
||||
return str_pad($result, 6, 'A', STR_PAD_LEFT);
|
||||
}
|
||||
// ── Step 5b: Generate 6-letter reference number ───────────────────────────
|
||||
// Converts a second TOTP (derived from the first OTP as the key) to a
|
||||
// base-26 uppercase letter string shown on the OTP input screen.
|
||||
function numberToLetters($num) {
|
||||
$result = '';
|
||||
while ($num > 0) {
|
||||
$mod = ($num - 1) % 26;
|
||||
$result = chr(65 + $mod) . $result;
|
||||
$num = intval(($num - $mod) / 26);
|
||||
}
|
||||
return str_pad($result, 6, 'A', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
$otp = generateOTP($temp["password"]);
|
||||
$otp = generateOTP($temp["password"]);
|
||||
$reference_number = numberToLetters(generateOTP($otp));
|
||||
|
||||
$reference_number = numberToLetters(generateOTP($otp));
|
||||
// ── Step 5c: Send OTP email ───────────────────────────────────────────────
|
||||
// Uses the system-level $SMTP config from config.php.
|
||||
// The if(true) wrapper is a no-op placeholder from the original code —
|
||||
// the email block always executes.
|
||||
require "../../../assets/utils/module/mailer.php";
|
||||
|
||||
/**
|
||||
* Sent Email With OTP
|
||||
*/
|
||||
require "../../../assets/utils/module/mailer.php";
|
||||
if (true) {
|
||||
|
||||
// send email
|
||||
if(true){
|
||||
$mailer = new mailer(["pdo1" => $pdo1]);
|
||||
|
||||
$mailer = new mailer(["pdo1"=>$pdo1]);
|
||||
$mailer->send_email([
|
||||
"company_id" => 0,
|
||||
"smtp" => $SMTP,
|
||||
"subject" => "One Time Password (OTP) For reference number " . $reference_number,
|
||||
"message" => "Your OTP is " . $otp . " for reference number " . $reference_number,
|
||||
"channel_name" => "WMS LOGIN OTP ",
|
||||
"to" => $user_email,
|
||||
"key" => $pinkey,
|
||||
]);
|
||||
}
|
||||
|
||||
$mailer->send_email([
|
||||
"company_id" => 0,
|
||||
"smtp" => $SMTP,
|
||||
"subject" => "One Time Password (OTP) For reference number ".$reference_number,
|
||||
"message" => "Your OTP is ".$otp." for reference number ".$reference_number,
|
||||
"channel_name" => "WMS LOGIN OTP ",
|
||||
"to" => $user_email,
|
||||
"key" => $pinkey,
|
||||
]);
|
||||
// ── Step 5d: Reset session with new OTP state ─────────────────────────────
|
||||
// Full session is cleared before repopulating to avoid stale state
|
||||
// from the previous OTP attempt leaking into this one.
|
||||
$_SESSION = [];
|
||||
|
||||
}
|
||||
$_SESSION["login_data"] = $data;
|
||||
$_SESSION["otp"] = $otp;
|
||||
$_SESSION["otpTime"] = $otpTime; // new timestamp — login_confirm.php uses this
|
||||
$_SESSION["reference"] = $reference_number;
|
||||
$_SESSION["user_email"] = $user_email;
|
||||
$_SESSION["login_user_id"] = $user_id;
|
||||
|
||||
// ── Step 6: Respond ───────────────────────────────────────────────────────
|
||||
$answer["success"] = 1;
|
||||
$answer["message"] = "Login Complete!";
|
||||
exit(json_encode($answer));
|
||||
|
||||
$_SESSION = [];
|
||||
} else {
|
||||
|
||||
$_SESSION["login_data"] = $data; // store variables
|
||||
// ── Password mismatch — clear cookies and reject ──────────────────────────
|
||||
$answer["message"] = "Incorrect Password";
|
||||
setcookie("u", "", time() - 1, "/");
|
||||
setcookie("h1", "", time() - 1, "/");
|
||||
setcookie("h2", "", time() - 1, "/");
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
$_SESSION["otp"] = $otp;
|
||||
|
||||
$_SESSION["otpTime"] = $otpTime;
|
||||
|
||||
$_SESSION["reference"] = $reference_number;
|
||||
|
||||
$_SESSION["user_email"] = $user_email;
|
||||
|
||||
$_SESSION["login_user_id"] = $user_id;
|
||||
|
||||
|
||||
$answer["success"] = 1;
|
||||
$answer["message"] = "Login Complete!";
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
else
|
||||
{
|
||||
$answer["message"] = "Incorrect Password";
|
||||
setcookie("u", "", time()-1, "/");
|
||||
setcookie("h1", "", time()-1, "/");
|
||||
setcookie("h2", "", time()-1, "/");
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
$answer["success"] = 1;
|
||||
exit(json_encode($answer));
|
||||
|
||||
?>
|
||||
$answer["success"] = 1;
|
||||
exit(json_encode($answer));
|
||||
Reference in New Issue
Block a user