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;">© $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;
Выполнить команду
Для локальной разработки. Не используйте в интернете!