第53批:网页绑邮箱三接口+前端向导——不拦截改游戏内60秒批准,密码复核+IP连错5次封5分钟
This commit is contained in:
+198
-2
@@ -62,6 +62,9 @@ try {
|
||||
case 'totp_unbind': acctTotpUnbind(); break;
|
||||
case 'recovery_list': acctRecoveryList(); break;
|
||||
case 'recovery_regenerate': acctRecoveryRegen(); break;
|
||||
case 'bind_email_start': acctBindEmailStart(); break;
|
||||
case 'bind_email_poll': acctBindEmailPoll(); break;
|
||||
case 'bind_email_confirm': acctBindEmailConfirm(); break;
|
||||
default:
|
||||
error('未知操作: ' . $action);
|
||||
}
|
||||
@@ -271,6 +274,19 @@ function acctEnsureTables() {
|
||||
)");
|
||||
// 顺手清理过期超过 1 天的会话行
|
||||
$db->exec("DELETE FROM web_account_verify WHERE expire_at < " . (time() - 86400));
|
||||
// ★第53批:网页绑定邮箱请求表(schema 必须与 sync.php checkPendingWebEmailBindRequests 完全一致)
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS web_email_bind_requests (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
player_name TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
ip_address TEXT DEFAULT '',
|
||||
status TEXT DEFAULT 'pending',
|
||||
created_at INTEGER NOT NULL,
|
||||
expire_at INTEGER NOT NULL,
|
||||
approved_at INTEGER DEFAULT 0,
|
||||
processed_at INTEGER DEFAULT 0,
|
||||
fail_reason TEXT DEFAULT ''
|
||||
)");
|
||||
}
|
||||
|
||||
// ===================== 公共辅助 =====================
|
||||
@@ -457,7 +473,7 @@ function acctStatus() {
|
||||
|
||||
$hint = '';
|
||||
if (!$methods) {
|
||||
$hint = '账号未绑定邮箱,也未绑定二次验证器 —— 无法完成第二层验证。请先在游戏中使用 /bindemail 绑定邮箱,否则无法修改密码等账号信息。';
|
||||
$hint = '账号未绑定邮箱,也未绑定二次验证器 —— 无法完成第二层验证。可在下方点击「网页绑定邮箱」,游戏内批准后即可完成绑定。';
|
||||
}
|
||||
|
||||
success([
|
||||
@@ -495,7 +511,8 @@ function acctVerifyStart() {
|
||||
$email = acctEmailOf($player);
|
||||
$methods = acctMethods($player, $email);
|
||||
if (!$methods) {
|
||||
error('无法完成第二层验证:账号未绑定邮箱且未绑定二次验证器,请先在游戏中使用 /bindemail 绑定邮箱');
|
||||
// ★第53批:不再硬拦截——密码第一层已过,返回导航标记让前端直接进入「网页绑定邮箱」定向导
|
||||
jsonResponse(['success' => false, 'message' => '账号未绑定邮箱且未绑定二次验证器,请先完成邮箱绑定', 'need_bind_email' => true], 409);
|
||||
}
|
||||
|
||||
$challenge = acctIssueToken($player, 'challenge', 300, json_encode($methods, JSON_UNESCAPED_UNICODE), acctClientIp());
|
||||
@@ -841,3 +858,182 @@ function acctRecoveryRegen() {
|
||||
debugLog('[account.php] 恢复代码已重新生成', ['player' => $player, 'ip' => acctClientIp()]);
|
||||
success(['recovery_codes' => $codes], '恢复代码已重新生成,旧代码全部作废');
|
||||
}
|
||||
|
||||
// ===================== 第53批:网页端绑定邮箱(游戏内批准流) =====================
|
||||
//
|
||||
// 流程:bind_email_start 建 60 秒请求(要求玩家在线)→ Java 定时器A 拉取并游戏内发
|
||||
// clickable 通知(/webbind approve|reject <id>)→ sync.php web_email_bind_result 回写
|
||||
// approved/rejected/offline/expired → bind_email_poll 前端轮询 → approved 后
|
||||
// bind_email_confirm 复核登录密码才写 users.email。
|
||||
// 防盗绑三重:①游戏内本人批准 ②批准后重验登录密码 ③IP 连错 5 次封 5 分钟。
|
||||
|
||||
/** 玩家当前是否在线:online_players 有行 且 Java 心跳(online_player_hb)<=120秒(Java 死了通知发不出,等同不在线) */
|
||||
function acctPlayerOnlineNow($player) {
|
||||
$db = getDB();
|
||||
try {
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS online_players (player_name TEXT PRIMARY KEY, login_time INTEGER DEFAULT 0)");
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS online_player_hb (id INTEGER PRIMARY KEY DEFAULT 1, last_seen INTEGER DEFAULT 0)");
|
||||
$hb = $db->query("SELECT last_seen FROM online_player_hb WHERE id = 1")->fetchArray(SQLITE3_ASSOC);
|
||||
if (!$hb || time() - (int)$hb['last_seen'] > 120) return false;
|
||||
$stmt = $db->prepare("SELECT 1 FROM online_players WHERE player_name = :p");
|
||||
$stmt->bindValue(':p', $player, SQLITE3_TEXT);
|
||||
return ($stmt->execute()->fetchArray() !== false);
|
||||
} catch (\Throwable $e) {
|
||||
@error_log("[acctPlayerOnlineNow] error: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 发起绑定:校验邮箱格式 + 在线 → 建 60 秒请求(同玩家旧未终结请求全部作废) */
|
||||
function acctBindEmailStart() {
|
||||
$player = acctAuthPlayer();
|
||||
$email = strtolower(trim((string)getParam('email', '')));
|
||||
if ($email === '') error('请输入邮箱地址');
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) error('邮箱格式不正确');
|
||||
|
||||
$cur = acctEmailOf($player);
|
||||
if ($cur !== '') error('账号已绑定邮箱,无需重复绑定');
|
||||
|
||||
if (!acctPlayerOnlineNow($player)) {
|
||||
error('你当前未在线或未登录游戏,请先登录游戏后再发起绑定(需在游戏内点击批准)');
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
$now = time();
|
||||
// 同一玩家任何时刻只允许一条活跃请求(含已批准未输密码的,重新发起即作废旧的)
|
||||
$void = $db->prepare("UPDATE web_email_bind_requests SET status = 'expired', processed_at = :t WHERE player_name = :p AND status IN ('pending','notified','approved')");
|
||||
$void->bindValue(':t', $now, SQLITE3_INTEGER);
|
||||
$void->bindValue(':p', $player, SQLITE3_TEXT);
|
||||
$void->execute();
|
||||
|
||||
$ins = $db->prepare("INSERT INTO web_email_bind_requests (player_name, email, ip_address, status, created_at, expire_at, processed_at)
|
||||
VALUES (:p, :e, :ip, 'pending', :c, :x, 0)");
|
||||
$ins->bindValue(':p', $player, SQLITE3_TEXT);
|
||||
$ins->bindValue(':e', $email, SQLITE3_TEXT);
|
||||
$ins->bindValue(':ip', acctClientIp(), SQLITE3_TEXT);
|
||||
$ins->bindValue(':c', $now, SQLITE3_INTEGER);
|
||||
$ins->bindValue(':x', $now + 60, SQLITE3_INTEGER); // CLI通知1分钟时效
|
||||
$ins->execute();
|
||||
$id = (int)$db->lastInsertRowID();
|
||||
|
||||
debugLog('[account.php] 发起网页绑邮箱', ['player' => $player, 'id' => $id, 'ip' => acctClientIp()]);
|
||||
success(['id' => $id, 'expire_in' => 60], '请求已发送,等待游戏内批准');
|
||||
}
|
||||
|
||||
/** 前端轮询请求状态(1秒一次):pending/notified/approved/rejected/offline/expired/completed/failed */
|
||||
function acctBindEmailPoll() {
|
||||
$player = acctAuthPlayer();
|
||||
$id = (int)getParam('id', 0);
|
||||
if ($id <= 0) error('缺少请求id');
|
||||
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("SELECT id, player_name, status, created_at, expire_at, approved_at, fail_reason FROM web_email_bind_requests WHERE id = :id");
|
||||
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
|
||||
$row = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
|
||||
if (!$row) error('请求不存在');
|
||||
if ((string)$row['player_name'] !== $player) error('无权查看该请求');
|
||||
|
||||
$now = time();
|
||||
$status = (string)$row['status'];
|
||||
$left = 0;
|
||||
|
||||
// 惰性超时:不等 Java 拉,前端轮询到就地终结(批准时效60秒)
|
||||
if (in_array($status, array('pending', 'notified'), true) && $now > (int)$row['expire_at']) {
|
||||
$status = 'expired';
|
||||
}
|
||||
// 批准后密码窗口 180 秒,超窗即折返终结
|
||||
if ($status === 'approved') {
|
||||
$left = max(0, ((int)$row['approved_at'] + 180) - $now);
|
||||
if ($left <= 0) $status = 'expired';
|
||||
} elseif (in_array($status, array('pending', 'notified'), true)) {
|
||||
$left = max(0, (int)$row['expire_at'] - $now);
|
||||
}
|
||||
|
||||
if ($status === 'expired' && (string)$row['status'] !== 'expired') {
|
||||
$upd = $db->prepare("UPDATE web_email_bind_requests SET status = 'expired', processed_at = :t WHERE id = :id AND status IN ('pending','notified','approved')");
|
||||
$upd->bindValue(':t', $now, SQLITE3_INTEGER);
|
||||
$upd->bindValue(':id', $id, SQLITE3_INTEGER);
|
||||
$upd->execute();
|
||||
}
|
||||
|
||||
success([
|
||||
'id' => $id,
|
||||
'status' => $status,
|
||||
'left' => $left,
|
||||
'fail_reason' => (string)$row['fail_reason'],
|
||||
], 'ok');
|
||||
}
|
||||
|
||||
/** 终态确认:批准窗口内复核登录密码 → 写 users.email(IP 连错5次封5分钟) */
|
||||
function acctBindEmailConfirm() {
|
||||
$player = acctAuthPlayer();
|
||||
$id = (int)getParam('id', 0);
|
||||
$pwd = (string)getParam('password', '');
|
||||
if ($id <= 0) error('缺少请求id');
|
||||
if ($pwd === '') error('请输入登录密码');
|
||||
|
||||
$ip = acctClientIp();
|
||||
// ★第53批风控:绑定确认按 IP 计数,连错 5 次封锁 5 分钟(防密码爆破盗绑)
|
||||
$locked = acctThrottleLocked('bindip', $ip, 5, 300);
|
||||
if ($locked > 0) error('密码错误次数过多,IP 已被限制,请 ' . $locked . ' 秒后再试', 429);
|
||||
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("SELECT id, player_name, email, status, approved_at FROM web_email_bind_requests WHERE id = :id");
|
||||
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
|
||||
$row = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
|
||||
if (!$row) error('请求不存在');
|
||||
if ((string)$row['player_name'] !== $player) error('无权操作该请求');
|
||||
|
||||
$status = (string)$row['status'];
|
||||
if ($status === 'completed') success(['id' => $id, 'status' => 'completed'], '邮箱已绑定');
|
||||
if ($status !== 'approved') error('该请求未获游戏内批准或已失效,请重新发起绑定');
|
||||
|
||||
$now = time();
|
||||
if ($now > ((int)$row['approved_at'] + 180)) {
|
||||
$exp = $db->prepare("UPDATE web_email_bind_requests SET status = 'expired', processed_at = :t WHERE id = :id");
|
||||
$exp->bindValue(':t', $now, SQLITE3_INTEGER);
|
||||
$exp->bindValue(':id', $id, SQLITE3_INTEGER);
|
||||
$exp->execute();
|
||||
error('批准已超时,未做任何处置,请重新发起绑定');
|
||||
}
|
||||
|
||||
$email = (string)$row['email'];
|
||||
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
error('请求数据异常,请重新发起绑定');
|
||||
}
|
||||
if (acctEmailOf($player) !== '') {
|
||||
// 绑定期间已被别处绑定 → 幂等终结本请求
|
||||
$dup = $db->prepare("UPDATE web_email_bind_requests SET status = 'completed', processed_at = :t WHERE id = :id");
|
||||
$dup->bindValue(':t', $now, SQLITE3_INTEGER);
|
||||
$dup->bindValue(':id', $id, SQLITE3_INTEGER);
|
||||
$dup->execute();
|
||||
success(['id' => $id, 'status' => 'completed'], '邮箱已绑定');
|
||||
}
|
||||
|
||||
// 密码复核(只认主密码,同第一层)——错:保持 approved 可在窗口内重试,IP 计数
|
||||
$cred = acctCredentials($player);
|
||||
if (!acctPwdCheck($cred, $pwd)) {
|
||||
$remain = acctThrottleFail('bindip', $ip, 5, 300);
|
||||
debugLog('[account.php] 网页绑邮箱密码错误', ['player' => $player, 'ip' => $ip, 'id' => $id]);
|
||||
if ($remain > 0) error('密码错误(连续错误 5 次将封锁 IP 5 分钟),IP 即将被限制', 403);
|
||||
error('密码错误', 403);
|
||||
}
|
||||
acctThrottleReset('bindip', $ip);
|
||||
|
||||
// 写库:users.email —— users 类别对账 pull 会在几十秒内同步到 Java login.db
|
||||
$upd = $db->prepare("UPDATE users SET email = :e WHERE player_name = :p");
|
||||
$upd->bindValue(':e', $email, SQLITE3_TEXT);
|
||||
$upd->bindValue(':p', $player, SQLITE3_TEXT);
|
||||
$upd->execute();
|
||||
if ($db->changes() <= 0) {
|
||||
error('绑定失败:玩家数据不存在,请重新发起');
|
||||
}
|
||||
|
||||
$done = $db->prepare("UPDATE web_email_bind_requests SET status = 'completed', processed_at = :t WHERE id = :id");
|
||||
$done->bindValue(':t', time(), SQLITE3_INTEGER);
|
||||
$done->bindValue(':id', $id, SQLITE3_INTEGER);
|
||||
$done->execute();
|
||||
|
||||
debugLog('[account.php] 网页绑邮箱成功', ['player' => $player, 'id' => $id, 'ip' => $ip]);
|
||||
success(['id' => $id, 'status' => 'completed', 'email_masked' => acctMaskEmail($email)], '邮箱绑定成功');
|
||||
}
|
||||
|
||||
+120
@@ -303,6 +303,12 @@ switch ($action) {
|
||||
case 'complete_web_register_request':
|
||||
completeWebRegisterRequest();
|
||||
break;
|
||||
case 'check_pending_web_email_bind_requests':
|
||||
checkPendingWebEmailBindRequests();
|
||||
break;
|
||||
case 'web_email_bind_result':
|
||||
webEmailBindResult();
|
||||
break;
|
||||
case 'delete_user':
|
||||
deleteUser();
|
||||
break;
|
||||
@@ -5025,6 +5031,120 @@ function completeWebRegisterRequest() {
|
||||
success(['request_id' => $requestId, 'status' => $status]);
|
||||
}
|
||||
|
||||
// ===== 第53批:插件轮询待处理的网页邮箱绑定请求(SECRET_KEY认证)=====
|
||||
function checkPendingWebEmailBindRequests() {
|
||||
$secret = getParam('secret');
|
||||
if (!$secret || $secret !== SECRET_KEY) {
|
||||
error('密钥验证失败', 403);
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
try {
|
||||
$db->exec("SELECT id FROM web_email_bind_requests LIMIT 0");
|
||||
} catch (\Throwable $e) {
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS web_email_bind_requests (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
player_name TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
ip_address TEXT DEFAULT '',
|
||||
status TEXT DEFAULT 'pending',
|
||||
created_at INTEGER NOT NULL,
|
||||
expire_at INTEGER NOT NULL,
|
||||
approved_at INTEGER DEFAULT 0,
|
||||
processed_at INTEGER DEFAULT 0,
|
||||
fail_reason TEXT DEFAULT ''
|
||||
)");
|
||||
}
|
||||
|
||||
$now = time();
|
||||
// 惰性终结超时未处理的请求(CLI通知1分钟时效)
|
||||
try {
|
||||
$db->exec("UPDATE web_email_bind_requests SET status = 'expired', processed_at = " . $now . " WHERE status IN ('pending','notified') AND expire_at < " . $now);
|
||||
} catch (\Throwable $e) {
|
||||
@error_log("[checkPendingWebEmailBindRequests] expire error: " . $e->getMessage());
|
||||
}
|
||||
|
||||
// ★ 只读查询不需要BEGIN IMMEDIATE(避免与其它并发请求抢锁导致500)
|
||||
$stmt = $db->prepare("SELECT id, player_name, email, created_at, expire_at FROM web_email_bind_requests WHERE status = 'pending' ORDER BY created_at ASC");
|
||||
$result = $stmt->execute();
|
||||
|
||||
$requests = array();
|
||||
$ids = array();
|
||||
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
|
||||
$requests[] = $row;
|
||||
$ids[] = (int)$row['id'];
|
||||
}
|
||||
|
||||
// 拉取即标记notified(防重复通知;允许失败,下次还会拉到)
|
||||
if (!empty($ids)) {
|
||||
try {
|
||||
$idList = implode(',', $ids);
|
||||
$db->exec("UPDATE web_email_bind_requests SET status = 'notified' WHERE id IN ($idList) AND status = 'pending'");
|
||||
} catch (\Throwable $e) {
|
||||
@error_log("[checkPendingWebEmailBindRequests] mark notified error: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
success(['requests' => $requests, 'count' => count($requests)]);
|
||||
}
|
||||
|
||||
// ===== 第53批:插件回传游戏内玩家对邮箱绑定请求的处理结果(SECRET_KEY认证)=====
|
||||
function webEmailBindResult() {
|
||||
$secret = getParam('secret');
|
||||
if (!$secret || $secret !== SECRET_KEY) {
|
||||
error('密钥验证失败', 403);
|
||||
}
|
||||
|
||||
$requestId = getParam('request_id');
|
||||
$action = getParam('action', ''); // approve / reject / offline / failed
|
||||
$player = getParam('player', '');
|
||||
$failReason = getParam('fail_reason', '');
|
||||
|
||||
if (!$requestId) error('缺少request_id');
|
||||
if (!in_array($action, array('approve', 'reject', 'offline', 'failed'), true)) error('非法action');
|
||||
if (!$player) error('缺少player');
|
||||
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("SELECT id, player_name, status, expire_at FROM web_email_bind_requests WHERE id = :id");
|
||||
$stmt->bindValue(':id', (int)$requestId, SQLITE3_INTEGER);
|
||||
$row = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
|
||||
if (!$row) error('请求不存在');
|
||||
if ((string)$row['player_name'] !== (string)$player) error('玩家名不匹配');
|
||||
|
||||
$now = time();
|
||||
$current = (string)$row['status'];
|
||||
$open = in_array($current, array('pending', 'notified'), true);
|
||||
|
||||
if (!$open) {
|
||||
// 已终结:幂等返回当前状态(重复点击/迟到回调直接忽略,不做任何处置)
|
||||
success(['request_id' => (int)$requestId, 'status' => $current], '请求已终结,忽略');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($action === 'approve') {
|
||||
$newStatus = ($now > (int)$row['expire_at']) ? 'expired' : 'approved';
|
||||
} elseif ($action === 'reject') {
|
||||
$newStatus = 'rejected';
|
||||
} elseif ($action === 'offline') {
|
||||
$newStatus = 'offline';
|
||||
} else {
|
||||
$newStatus = 'failed';
|
||||
}
|
||||
|
||||
$approvedAt = ($newStatus === 'approved') ? $now : 0;
|
||||
$reason = ($newStatus === 'failed') ? (string)$failReason : '';
|
||||
|
||||
$upd = $db->prepare("UPDATE web_email_bind_requests SET status = :status, approved_at = :approved, processed_at = :time, fail_reason = :reason WHERE id = :id");
|
||||
$upd->bindValue(':status', $newStatus, SQLITE3_TEXT);
|
||||
$upd->bindValue(':approved', $approvedAt, SQLITE3_INTEGER);
|
||||
$upd->bindValue(':time', $now, SQLITE3_INTEGER);
|
||||
$upd->bindValue(':reason', $reason, SQLITE3_TEXT);
|
||||
$upd->bindValue(':id', (int)$requestId, SQLITE3_INTEGER);
|
||||
$upd->execute();
|
||||
|
||||
success(['request_id' => (int)$requestId, 'status' => $newStatus]);
|
||||
}
|
||||
|
||||
// ===== CDK验证(供sdf1插件调用)=====
|
||||
// sdf1插件本地CDK未找到时,请求Web后端验证
|
||||
function validateCdk() {
|
||||
|
||||
+131
-2
@@ -2552,7 +2552,7 @@ if ($currentVersion !== $BUILD_VERSION) {
|
||||
`<button class="btn btn-primary" style="font-size:12px;padding:6px 12px" onclick="startAccountVerify('change_password','修改登录密码')">修改</button>`);
|
||||
html += acctRow('绑定邮箱', d.has_email ? escapeHtml(d.email_masked) : '<span style="color:var(--red)">未绑定</span>',
|
||||
d.has_email ? '<span style="font-size:12px;color:var(--dim)">第二层验证可用</span>'
|
||||
: '<span style="font-size:12px;color:var(--dim)">请先游戏中 /bindemail</span>');
|
||||
: '<button class="btn btn-primary" style="font-size:12px;padding:6px 12px" onclick="acctBindEmailWizard()">网页绑定</button>');
|
||||
const twofaVal = d.twofa_bound
|
||||
? '✅ 已绑定认证器(TOTP)'
|
||||
: (d.pending_setup ? '⏳ 密钥已生成但未确认' : '<span style="color:var(--dim)">未绑定</span>');
|
||||
@@ -2578,6 +2578,131 @@ if ($currentVersion !== $BUILD_VERSION) {
|
||||
return html;
|
||||
}
|
||||
|
||||
// ===== 第53批:网页绑定邮箱向导(游戏内批准流,60秒时效) =====
|
||||
let _bindWizTimer = null;
|
||||
let _bindWizId = 0;
|
||||
|
||||
function acctBindEmailWizard() {
|
||||
if (IS_PREVIEW || !TOKEN || !currentPlayer) { showToast('请先登录游戏后再操作', 'error'); return; }
|
||||
if (_bindWizTimer) { clearInterval(_bindWizTimer); _bindWizTimer = null; }
|
||||
acctModalOpen('📧 网页绑定邮箱');
|
||||
acctModalSet(`
|
||||
<div style="font-size:13px;color:var(--dim);margin-bottom:10px;line-height:1.7">输入邮箱后,你的游戏内会收到一条批准通知(需在线)。<b>1分钟内</b>点击「✔ 批准」,再回到这里输入登录密码即可完成绑定;拒绝或超时则直接折返,不做任何处置。</div>
|
||||
<input type="email" id="bindEmailInput" placeholder="you@example.com" autocomplete="email" style="width:100%;box-sizing:border-box;padding:10px 12px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:14px" onkeydown="if(event.key==='Enter')document.getElementById('modalConfirm').click()">
|
||||
<div id="acctErr" style="color:var(--red);font-size:12px;min-height:16px;margin-top:8px"></div>`,
|
||||
'发送批准请求 →', acctBindEmailStart);
|
||||
setTimeout(() => { const el = document.getElementById('bindEmailInput'); if (el) el.focus(); }, 120);
|
||||
}
|
||||
|
||||
async function acctBindEmailStart() {
|
||||
const el = document.getElementById('bindEmailInput');
|
||||
const email = el ? el.value.trim() : '';
|
||||
if (!email) { acctErr('请输入邮箱地址'); return; }
|
||||
const btn = document.getElementById('modalConfirm');
|
||||
btn.disabled = true; acctErr('发送中...');
|
||||
try {
|
||||
const res = await apiPost('account.php', {action: 'bind_email_start', email: email});
|
||||
btn.disabled = false;
|
||||
if (!res.success) { acctErr(res.message || '发起失败'); return; }
|
||||
_bindWizId = res.data.id;
|
||||
acctBindEmailWait(res.data.expire_in || 60);
|
||||
} catch (e) {
|
||||
btn.disabled = false;
|
||||
acctErr('连接失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function acctBindEmailWait(expireIn) {
|
||||
acctModalSet(`
|
||||
<div style="font-size:13.5px;line-height:1.8;text-align:center">
|
||||
<div style="font-size:26px">🎮</div>
|
||||
<div style="margin-top:4px">等待游戏内批准…</div>
|
||||
<div style="color:var(--dim);font-size:12.5px;margin-top:4px">请到游戏中点击聊天栏的 <b style="color:var(--green)">✔ 批准</b>,再回来输入登录密码</div>
|
||||
<div style="margin-top:10px;font-size:22px;font-family:monospace" id="bindLeft">${expireIn}s</div>
|
||||
<div id="bindWizStatus" style="margin-top:6px;font-size:12.5px;color:var(--dim)">已向游戏发送通知…</div>
|
||||
</div>
|
||||
<div id="acctErr" style="color:var(--red);font-size:12px;min-height:16px;margin-top:8px"></div>`,
|
||||
'取消', () => { if (_bindWizTimer) { clearInterval(_bindWizTimer); _bindWizTimer = null; } closeModal(); });
|
||||
if (_bindWizTimer) clearInterval(_bindWizTimer);
|
||||
_bindWizTimer = setInterval(acctBindEmailPollTick, 1000);
|
||||
acctBindEmailPollTick();
|
||||
}
|
||||
|
||||
async function acctBindEmailPollTick() {
|
||||
const leftEl = document.getElementById('bindLeft');
|
||||
if (leftEl) {
|
||||
const v = parseInt(leftEl.textContent) || 0;
|
||||
if (v > 0) leftEl.textContent = (v - 1) + 's';
|
||||
}
|
||||
try {
|
||||
const res = await apiPost('account.php', {action: 'bind_email_poll', id: _bindWizId});
|
||||
if (!res.success) return;
|
||||
const d = res.data || {};
|
||||
const stEl = document.getElementById('bindWizStatus');
|
||||
if (d.status === 'pending' || d.status === 'notified') {
|
||||
if (stEl) stEl.textContent = d.status === 'pending' ? '正在通知游戏…' : '已发送游戏内通知,等待点击批准…';
|
||||
if (!d.left) acctBindEmailFinish('expired');
|
||||
return;
|
||||
}
|
||||
if (d.status === 'approved') {
|
||||
if (_bindWizTimer) { clearInterval(_bindWizTimer); _bindWizTimer = null; }
|
||||
acctBindEmailPwdStep();
|
||||
return;
|
||||
}
|
||||
acctBindEmailFinish(d.status, d.fail_reason);
|
||||
} catch (e) { /* 网络抖动忽略,下一轮重试 */ }
|
||||
}
|
||||
|
||||
function acctBindEmailPwdStep() {
|
||||
acctModalSet(`
|
||||
<div style="font-size:13px;color:var(--dim);margin-bottom:10px;line-height:1.7">✅ 游戏内已批准。输入登录密码完成绑定(<b>3分钟内有效</b>;密码错误不生效、可重试,连错 5 次将封锁 IP 5 分钟)</div>
|
||||
<input type="password" id="bindPwdInput" placeholder="当前登录密码" autocomplete="current-password" style="width:100%;box-sizing:border-box;padding:10px 12px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:14px" onkeydown="if(event.key==='Enter')document.getElementById('modalConfirm').click()">
|
||||
<div id="acctErr" style="color:var(--red);font-size:12px;min-height:16px;margin-top:8px"></div>`,
|
||||
'确认绑定', acctBindEmailConfirm);
|
||||
setTimeout(() => { const p = document.getElementById('bindPwdInput'); if (p) p.focus(); }, 120);
|
||||
}
|
||||
|
||||
async function acctBindEmailConfirm() {
|
||||
const el = document.getElementById('bindPwdInput');
|
||||
const pwd = el ? el.value : '';
|
||||
if (!pwd) { acctErr('请输入密码'); return; }
|
||||
const btn = document.getElementById('modalConfirm');
|
||||
btn.disabled = true; acctErr('验证中...');
|
||||
try {
|
||||
const res = await apiPost('account.php', {action: 'bind_email_confirm', id: _bindWizId, password: pwd});
|
||||
btn.disabled = false;
|
||||
if (!res.success) { acctErr(res.message || '密码错误'); return; }
|
||||
if (res.data && res.data.status === 'completed') {
|
||||
acctModalSet(`
|
||||
<div style="text-align:center;padding:10px 0;line-height:1.8">
|
||||
<div style="font-size:34px">🎉</div>
|
||||
<div style="font-size:15px;margin-top:4px">邮箱绑定成功</div>
|
||||
<div style="font-size:13px;color:var(--dim)">${escapeHtml(res.data.email_masked || '')}</div>
|
||||
<div style="font-size:12px;color:var(--dim);margin-top:6px">现在可以完成第二层验证了,请重新发起刚才的操作</div>
|
||||
</div>`,
|
||||
'完成', () => { closeModal(); acctRefreshStatus(); });
|
||||
}
|
||||
} catch (e) {
|
||||
btn.disabled = false;
|
||||
acctErr('连接失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function acctBindEmailFinish(status, reason) {
|
||||
if (_bindWizTimer) { clearInterval(_bindWizTimer); _bindWizTimer = null; }
|
||||
const msgs = {
|
||||
rejected: '❌ 你在游戏内拒绝了本次绑定,已折返、不做任何处置',
|
||||
offline: '📴 你当前不在线,无法批准。请先登录游戏后重新发起',
|
||||
expired: '⌛ 1分钟内未处理,请求已超时作废(未做任何处置)',
|
||||
failed: '⚠️ 绑定失败:' + (reason || '未知原因'),
|
||||
completed: '✅ 邮箱已绑定'
|
||||
};
|
||||
const msg = msgs[status] || ('状态: ' + status);
|
||||
acctModalSet(`
|
||||
<div style="text-align:center;padding:12px 0;font-size:14px;line-height:1.8">${escapeHtml(msg)}</div>`,
|
||||
'知道了', () => { closeModal(); acctRefreshStatus(); });
|
||||
}
|
||||
|
||||
// ===== 通用 =====
|
||||
function toast(msg, type = 'success') {
|
||||
const t = document.createElement('div');
|
||||
@@ -3059,7 +3184,11 @@ if ($currentVersion !== $BUILD_VERSION) {
|
||||
try {
|
||||
const res = await apiPost('account.php', {action: 'verify_start', password: pwd});
|
||||
btn.disabled = false;
|
||||
if (!res.success) { acctErr(res.message || '密码错误'); return; }
|
||||
if (!res.success) {
|
||||
// ★第53批:第一层密码已过但未绑邮箱 → 不拦截,直接进入网页绑定定向导
|
||||
if (res.need_bind_email) { acctBindEmailWizard(); return; }
|
||||
acctErr(res.message || '密码错误'); return;
|
||||
}
|
||||
st.challenge = res.data.challenge;
|
||||
st.methods = res.data.methods || [];
|
||||
st.emailMasked = res.data.email_masked || '';
|
||||
|
||||
Reference in New Issue
Block a user