PHP WebShell

Текущая директория: /var/www/bitcardoApp/models/auth

Просмотр файла: forgot_process.php

<?php
// models/auth/forgot_process.php — Issue reset code (email OTP, channel='reset')
require_once __DIR__ . '/../../config/bootstrap.php';

$rateLib = __DIR__ . '/../../lib/rate_limit.php';
if (file_exists($rateLib)) require_once $rateLib;

$useMailer = file_exists(__DIR__ . '/../../lib/mailer.php');
if ($useMailer) require_once __DIR__ . '/../../lib/mailer.php';

// ------- helpers -------
function back_err($msg){
  $_SESSION['flash'] = ['error' => $msg];
  header('Location: /auth/forgot.php'); exit;
}
function go_reset_ok($msg, $login=''){
  $_SESSION['flash'] = ['ok' => $msg];
  if ($login !== '') $_SESSION['prefill_login'] = $login;
  header('Location: /auth/reset.php'); exit;
}
function rl_on(){ return function_exists('is_enabled') ? is_enabled('rate_limit_enabled', true) : true; }
function norm_login($login){
  // Use rl_norm_login if available; otherwise lowercase+trim
  return function_exists('rl_norm_login') ? rl_norm_login($login) : strtolower(trim($login));
}

// ------- CSRF -------
if (!isset($_POST['csrf'], $_SESSION['csrf']) || !hash_equals($_SESSION['csrf'], $_POST['csrf'])) {
  back_err('Session expired. Please try again.');
}

// ------- input -------
$login = trim($_POST['login'] ?? '');
if ($login === '') back_err('Enter your email or phone.');
$loginTok = norm_login($login);

// ------- rate limit (send) -------
if (rl_on() && function_exists('rl_check_and_inc')) {
  // 5 attempts / 15 mins; lock for 15 mins
  $r = rl_check_and_inc($conn, 'reset_send', $loginTok, 5, 900, 900);
  if (!$r['ok']) back_err("Too many requests. Try again in {$r['locked_for']}s.");
}

// ------- find user (no enumeration) -------
$stmt = $conn->prepare("SELECT user_id, email FROM users WHERE email=? OR phone=? LIMIT 1");
$stmt->bind_param('ss', $login, $login);
$stmt->execute();
$stmt->bind_result($uid, $email);
$found = $stmt->fetch();
$stmt->close();

$genericOk = 'If that account exists, a reset code has been sent. Check your email.';

// If not found, still proceed to reset page (generic).
if (!$found || !$uid || !$email) {
  go_reset_ok($genericOk, $login);
}

// ------- invalidate previous unconsumed reset codes -------
$conn->query("UPDATE user_otps SET consumed_at=NOW() WHERE user_id=".(int)$uid." AND channel='reset' AND consumed_at IS NULL");

// ------- generate code -------
$code     = str_pad((string)random_int(0, 999999), 6, '0', STR_PAD_LEFT);
$selector = bin2hex(random_bytes(12));
$hash     = hash('sha256', $code);
$ip       = $_SERVER['REMOTE_ADDR'] ?? '';
$expiry   = (new DateTimeImmutable('+15 minutes'))->format('Y-m-d H:i:s');

// ------- save OTP -------
$ins = $conn->prepare("INSERT INTO user_otps (user_id, channel, selector, token_hash, expires_at, ip)
                       VALUES (?, 'reset', ?, ?, ?, ?)");
if (!$ins) { error_log('[RESET] prepare failed: '.$conn->error); go_reset_ok($genericOk, $login); }
$ins->bind_param('issss', $uid, $selector, $hash, $expiry, $ip);
$ok = $ins->execute();
$ins->close();
if (!$ok) { error_log('[RESET] insert failed: '.$conn->error); go_reset_ok($genericOk, $login); }

// ------- email body -------
$brand = defined('SMTP_FROM_NAME') ? SMTP_FROM_NAME : 'Bitcardo';
$year  = date('Y');
$codeSpaced = implode(' ', str_split($code, 3));
$html = <<<HTML
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f6f8fb;padding:24px 0;">
  <tr><td align="center">
    <table role="presentation" width="560" cellpadding="0" cellspacing="0" style="background:#ffffff;border-radius:12px;border:1px solid #e8eef3;box-shadow:0 4px 18px rgba(7,98,137,.06);font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;">
      <tr><td style="padding:24px 24px 0 24px;text-align:center;">
        <div style="display:inline-block;background:#076289;color:#fff;font-weight:700;padding:8px 14px;border-radius:999px;">$brand Password Reset</div>
      </td></tr>
      <tr><td style="padding:16px 24px 4px 24px;text-align:center;">
        <h1 style="margin:0;font-size:20px;color:#0f172a;">Verify it’s you</h1>
        <p style="margin:8px 0 0 0;color:#334155;font-size:14px;line-height:1.5">Use this code to reset your password:</p>
      </td></tr>
      <tr><td style="padding:8px 24px 24px 24px;text-align:center;">
        <div style="display:inline-block;border:2px dashed #076289;border-radius:12px;padding:14px 18px;">
          <div style="font-size:28px;font-weight:800;letter-spacing:4px;color:#0f172a;">$codeSpaced</div>
          <div style="color:#64748b;font-size:12px;margin-top:6px;">Expires in 15 minutes</div>
        </div>
      </td></tr>
      <tr><td style="padding:0 24px 24px 24px;text-align:center;">
        <div style="color:#475569;font-size:13px;line-height:1.6">
          Didn’t request this? You can safely ignore this email.
        </div>
      </td></tr>
      <tr><td style="padding:16px 24px 24px 24px;border-top:1px solid #e8eef3;text-align:center;color:#94a3b8;font-size:12px;">&copy; $year $brand</td></tr>
    </table>
  </td></tr>
</table>
HTML;

// ------- send (or show dev) -------
$sent = false;
if ($useMailer && function_exists('send_email')) {
  $sent = send_email($email, "$brand password reset code", $html);
} else {
  $headers  = "MIME-Version: 1.0\r\n";
  $headers .= "Content-type: text/html; charset=UTF-8\r\n";
  $from = defined('SMTP_FROM') ? SMTP_FROM : 'no-reply@bitcardo.com';
  $fromName = defined('SMTP_FROM_NAME') ? SMTP_FROM_NAME : 'Bitcardo';
  $headers .= "From: {$fromName} <{$from}>\r\n";
  $sent = @mail($email, "$brand password reset code", $html, $headers);
}

// DEV: session + URL fallback so the code ALWAYS shows on /auth/reset.php
$devQS = '';
if ((defined('OTP_ALWAYS_SHOW_DEV') && OTP_ALWAYS_SHOW_DEV) ||
    (!$sent && defined('OTP_DEV_MODE') && OTP_DEV_MODE)) {
  $_SESSION['__DEV_RESET_CODE__'] = $code;
  $devQS = '?dev_code=' . urlencode($code);
}

// Redirect to reset page with generic OK + prefilled login
$_SESSION['flash'] = ['ok' => $genericOk];
$_SESSION['prefill_login'] = $login;
header('Location: /auth/reset.php' . $devQS);
exit;

Выполнить команду


Для локальной разработки. Не используйте в интернете!