移除登录失败限流;修复连接信息被 e2e 串味为 127.0.0.1
- bootstrap.php/login.php:整体移除「5 次失败锁 10 分钟」限流,登录失败仅即时提示原因 - 新增 tools/fix-config.php:一键双写修正连接地址(默认 exam.nxxy335.top),清空账号密码让用户重输真实卷王凭据 - NY_DATA 隔离落地:run-all.sh 给写配置的 e2e 指向临时目录;修 Windows Git Bash /c/... 路径规范化 bug;.ny-test-data 加 .gitignore - 清理 dead throttle 引用(e2e 脚本 + docs) - 修 login.php 真实 bug:ny_is_auth() 分支在加载 $cfg 前引用导致 Undefined variable 警告 - 全量测试 116 通过(PHP 44 + e2e 72)
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
# 运行时数据:含账号密码、API 令牌、学生名单,禁止入库
|
# 运行时数据:含账号密码、API 令牌、学生名单,禁止入库
|
||||||
data/
|
data/
|
||||||
|
|
||||||
|
# e2e 隔离用的临时数据目录(run-all.sh 自动创建,不入库)
|
||||||
|
.ny-test-data/
|
||||||
|
|
||||||
# 依赖与系统文件
|
# 依赖与系统文件
|
||||||
node_modules/
|
node_modules/
|
||||||
vendor/
|
vendor/
|
||||||
|
|||||||
+11
-48
@@ -9,7 +9,14 @@ const NY_APP_NAME = 'NY问卷考试系统数据平台';
|
|||||||
const NY_VERSION = '1.0.0';
|
const NY_VERSION = '1.0.0';
|
||||||
|
|
||||||
define('NY_ROOT', str_replace('\\', '/', __DIR__));
|
define('NY_ROOT', str_replace('\\', '/', __DIR__));
|
||||||
define('NY_DATA', NY_ROOT . '/data');
|
// 数据目录默认指向项目下的 data/;测试可通过 NY_DATA 环境变量整体重定向到临时目录,
|
||||||
|
// 避免 e2e 把 mock 服务的连接配置写进生产库(曾因此把生产 base_url 改成 127.0.0.1 导致登录失败)。
|
||||||
|
$nyData = NY_ROOT . '/data';
|
||||||
|
$nyDataEnv = getenv('NY_DATA');
|
||||||
|
if ($nyDataEnv !== false && $nyDataEnv !== '') {
|
||||||
|
$nyData = rtrim(str_replace('\\', '/', $nyDataEnv), '/');
|
||||||
|
}
|
||||||
|
define('NY_DATA', $nyData);
|
||||||
|
|
||||||
// ---------------- 环境预检 ----------------
|
// ---------------- 环境预检 ----------------
|
||||||
if (PHP_VERSION_ID < 80000) {
|
if (PHP_VERSION_ID < 80000) {
|
||||||
@@ -141,53 +148,9 @@ function ny_csrf_ok(): bool
|
|||||||
return $given !== '' && hash_equals($token, $given);
|
return $given !== '' && hash_equals($token, $given);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------- 登录失败限流 ----------------
|
// ---------------- 登录失败限流(已移除) ----------------
|
||||||
/**
|
// 注:单机内部工具场景下,5 次失败锁 10 分钟的限制常因配置写错/测环境串味而把正常用户挡在门外,
|
||||||
* 登录失败限流:按 IP 记录连续失败次数与时间戳(写入 data/login_fail.json)。
|
// 且无实际爆破价值(仅内网使用)。应需求已整体移除:登录不再限流,失败只即时提示原因。
|
||||||
* 5 次失败后锁 10 分钟。仅防御暴力破解,不做超强语义(单机小工具足够)。
|
|
||||||
*/
|
|
||||||
function ny_login_throttle_check(): array
|
|
||||||
{
|
|
||||||
$path = NY_DATA . '/login_fail.json';
|
|
||||||
$ip = (string)($_SERVER['REMOTE_ADDR'] ?? 'unknown');
|
|
||||||
$rec = ['count' => 0, 'until' => 0];
|
|
||||||
if (is_file($path)) {
|
|
||||||
$j = json_decode((string)file_get_contents($path), true);
|
|
||||||
if (is_array($j) && isset($j[$ip])) {
|
|
||||||
$rec = array_merge($rec, $j[$ip]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $rec;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ny_login_throttle_hit(): void
|
|
||||||
{
|
|
||||||
$path = NY_DATA . '/login_fail.json';
|
|
||||||
$ip = (string)($_SERVER['REMOTE_ADDR'] ?? 'unknown');
|
|
||||||
$all = is_file($path) ? (json_decode((string)file_get_contents($path), true) ?: []) : [];
|
|
||||||
$rec = isset($all[$ip]) ? $all[$ip] : ['count' => 0, 'until' => 0];
|
|
||||||
$rec['count'] = ((int)($rec['count'] ?? 0)) + 1;
|
|
||||||
if ($rec['count'] >= 5) {
|
|
||||||
$rec['count'] = 0; // 重新计数,进入冷却
|
|
||||||
$rec['until'] = time() + 600; // 锁 10 分钟
|
|
||||||
}
|
|
||||||
$all[$ip] = $rec;
|
|
||||||
@file_put_contents($path, json_encode($all, JSON_UNESCAPED_UNICODE), LOCK_EX);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ny_login_throttle_reset(): void
|
|
||||||
{
|
|
||||||
$path = NY_DATA . '/login_fail.json';
|
|
||||||
$ip = (string)($_SERVER['REMOTE_ADDR'] ?? 'unknown');
|
|
||||||
if (!is_file($path)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$all = json_decode((string)file_get_contents($path), true) ?: [];
|
|
||||||
if (isset($all[$ip])) {
|
|
||||||
unset($all[$ip]);
|
|
||||||
@file_put_contents($path, json_encode($all, JSON_UNESCAPED_UNICODE), LOCK_EX);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 判断是否已安装(只看文件 config:base_url 非空即视为已具备连接配置) */
|
/** 判断是否已安装(只看文件 config:base_url 非空即视为已具备连接配置) */
|
||||||
function ny_is_installed(): bool
|
function ny_is_installed(): bool
|
||||||
|
|||||||
@@ -64,8 +64,6 @@ e2e 会备份并修改 `data/ny.db`、`data/config.json`、`data/roster.json`,
|
|||||||
```bash
|
```bash
|
||||||
git status data/ # data/ 已被 gitignore,需手工核对
|
git status data/ # data/ 已被 gitignore,需手工核对
|
||||||
```
|
```
|
||||||
|
|
||||||
另外测试前请删除 `data/login_fail.json`,否则可能触发登录限流。
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## 写新测试时注意
|
## 写新测试时注意
|
||||||
|
|||||||
+3
-2
@@ -18,9 +18,10 @@
|
|||||||
务必从登录响应的 `Set-Cookie` 中取出 `sk-token`,并在后续每个请求带上
|
务必从登录响应的 `Set-Cookie` 中取出 `sk-token`,并在后续每个请求带上
|
||||||
`Cookie: sk-token=<token>`。本平台已处理此细节。
|
`Cookie: sk-token=<token>`。本平台已处理此细节。
|
||||||
|
|
||||||
**Q:登录一直失败并被锁住**
|
**Q:登录一直失败**
|
||||||
|
|
||||||
连续 5 次失败会锁定 10 分钟。等待即可;如需立即解锁,删除 `data/login_fail.json`。
|
登录失败只会在登录页即时提示原因(账号/密码错、地址不可达等),不会因多次失败被锁。
|
||||||
|
若登录页连接信息显示成 `127.0.0.1` 等错误地址,用 `php tools/fix-config.php` 修正即可。
|
||||||
|
|
||||||
**Q:卷王改了密码后平台一直提示未登录**
|
**Q:卷王改了密码后平台一直提示未登录**
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -40,7 +40,7 @@ features:
|
|||||||
details: 零依赖生成 .xlsx,支持多工作表、最新成绩视图、成绩明细与缺考名单打印。
|
details: 零依赖生成 .xlsx,支持多工作表、最新成绩视图、成绩明细与缺考名单打印。
|
||||||
- icon: 🔒
|
- icon: 🔒
|
||||||
title: 安全内建
|
title: 安全内建
|
||||||
details: 会话登录、CSRF 令牌、登录失败限流,上传文件白名单 + 随机名 + 解析后即删。
|
details: 会话登录、CSRF 令牌,上传文件白名单 + 随机名 + 解析后即删。
|
||||||
---
|
---
|
||||||
|
|
||||||
## 三分钟了解它
|
## 三分钟了解它
|
||||||
|
|||||||
@@ -11,11 +11,11 @@
|
|||||||
- 卷王令牌缓存在 `data/cache/token.json`;令牌失效时平台会自动重新登录;
|
- 卷王令牌缓存在 `data/cache/token.json`;令牌失效时平台会自动重新登录;
|
||||||
若卷王侧也认证失败,前端会先调用 `logout.php` 清会话再跳转登录页,避免出现跳转死循环。
|
若卷王侧也认证失败,前端会先调用 `logout.php` 清会话再跳转登录页,避免出现跳转死循环。
|
||||||
|
|
||||||
## 登录限流
|
## 登录失败处理
|
||||||
|
|
||||||
连续 **5 次**登录失败 → 锁定 **10 分钟**。状态记录在 `data/login_fail.json`。
|
登录失败(卷王账号/密码错、地址不可达等)只会在登录页即时提示原因,**不做失败次数限流**——
|
||||||
|
单机内网工具场景下,限流常因配置写错或测试串味把正常用户挡在门外,且无实际防爆破价值。
|
||||||
应急解锁:删除 `data/login_fail.json`。
|
若怀疑连接配置被测试污染导致连不上真实卷王,用 `php tools/fix-config.php` 一键修正连接地址。
|
||||||
|
|
||||||
## CSRF 防护
|
## CSRF 防护
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
require __DIR__ . '/bootstrap.php';
|
require __DIR__ . '/bootstrap.php';
|
||||||
|
|
||||||
|
// 先加载配置:下面已登录分支要用 $cfg 构造 SurveyKingClient 探活,必须在前面就准备好
|
||||||
|
$cfg = ny_load_config();
|
||||||
|
|
||||||
if (ny_is_auth()) {
|
if (ny_is_auth()) {
|
||||||
// 已登录但卷王 token 可能已过期:用当前 token 探一次卷王 /system,能 200 即视为有效,否则清掉本地会话强制重新登录
|
// 已登录但卷王 token 可能已过期:用当前 token 探一次卷王 /system,能 200 即视为有效,否则清掉本地会话强制重新登录
|
||||||
try {
|
try {
|
||||||
@@ -23,42 +26,32 @@ if (ny_is_auth()) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$cfg = ny_load_config();
|
|
||||||
$err = '';
|
$err = '';
|
||||||
$ok = false;
|
$ok = false;
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
// 登录失败限流:5 次失败锁 10 分钟,防止暴力破解
|
$user = trim((string)($_POST['username'] ?? $cfg['username']));
|
||||||
$throttle = ny_login_throttle_check();
|
$pwd = (string)($_POST['password'] ?? '');
|
||||||
if ($throttle['until'] > time()) {
|
// 允许用户在本页调整账号;密码必须输入
|
||||||
$wait = (int)(($throttle['until'] - time()) / 60) + 1;
|
if ($user === '' || $pwd === '') {
|
||||||
$err = '尝试次数过多,请 ' . $wait . ' 分钟后再试。';
|
$err = '请输入账号与密码。';
|
||||||
} else {
|
} else {
|
||||||
$user = trim((string)($_POST['username'] ?? $cfg['username']));
|
// 用当前配置的连接信息 + 本页输入的账密做一次卷王登录
|
||||||
$pwd = (string)($_POST['password'] ?? '');
|
$test = $cfg;
|
||||||
// 允许用户在本页调整账号;密码必须输入
|
$test['username'] = $user;
|
||||||
if ($user === '' || $pwd === '') {
|
$test['password'] = $pwd;
|
||||||
$err = '请输入账号与密码。';
|
try {
|
||||||
} else {
|
$client = new SurveyKingClient($test);
|
||||||
// 用当前配置的连接信息 + 本页输入的账密做一次卷王登录
|
$client->getToken(true);
|
||||||
$test = $cfg;
|
// 登录成功:把账号与密码写回配置(本地单用户工具,持久化密码以便 token
|
||||||
$test['username'] = $user;
|
// 失效时平台能自动重新登录卷王,避免后台刷新陷入"未配置账号"的死状态)
|
||||||
$test['password'] = $pwd;
|
$cfg['username'] = $user;
|
||||||
try {
|
$cfg['password'] = $pwd;
|
||||||
$client = new SurveyKingClient($test);
|
ny_save_config($cfg);
|
||||||
$client->getToken(true);
|
ny_login();
|
||||||
// 登录成功:把账号与密码写回配置(本地单用户工具,持久化密码以便 token
|
$ok = true;
|
||||||
// 失效时平台能自动重新登录卷王,避免后台刷新陷入"未配置账号"的死状态)
|
} catch (Throwable $e) {
|
||||||
$cfg['username'] = $user;
|
$err = '登录失败:' . $e->getMessage();
|
||||||
$cfg['password'] = $pwd;
|
|
||||||
ny_save_config($cfg);
|
|
||||||
ny_login();
|
|
||||||
ny_login_throttle_reset();
|
|
||||||
$ok = true;
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
ny_login_throttle_hit();
|
|
||||||
$err = '登录失败:' . $e->getMessage();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-3
@@ -26,11 +26,19 @@ done
|
|||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "================ Node e2e(需 mock-server + PHP 内置服务器) ================"
|
echo "================ Node e2e(需 mock-server + PHP 内置服务器) ================"
|
||||||
for f in tools/test-config-save.js tools/test-draw-alloc.js tools/test-upload-guard.js; do
|
# e2e 会把 mock 连接配置写进数据目录;用 NY_DATA 把这两个会改写配置的测试整体指向临时目录,
|
||||||
|
# 彻底避免污染生产 data/ny.db(曾因此把登录页连接信息写成 127.0.0.1)。
|
||||||
|
NY_DATA_TMP="$(pwd)/.ny-test-data"
|
||||||
|
mkdir -p "$NY_DATA_TMP"
|
||||||
|
for f in tools/test-config-save.js tools/test-upload-guard.js; do
|
||||||
echo ""
|
echo ""
|
||||||
echo ">>> $f"
|
echo ">>> $f (NY_DATA=$NY_DATA_TMP)"
|
||||||
"$NODE" "$f"
|
NY_DATA="$NY_DATA_TMP" "$NODE" "$f"
|
||||||
done
|
done
|
||||||
|
# test-draw-alloc.js 不读写连接配置,沿用项目 data/ 即可
|
||||||
|
echo ""
|
||||||
|
echo ">>> tools/test-draw-alloc.js"
|
||||||
|
"$NODE" "tools/test-draw-alloc.js"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "全部测试完成。"
|
echo "全部测试完成。"
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 修复 / 重置系统连接配置(终端一次性工具)
|
||||||
|
*
|
||||||
|
* 背景:
|
||||||
|
* 连接配置同时存在于 data/ny.db(SQLite,加载时优先)与 data/config.json(文件备份)。
|
||||||
|
* ny_load_config() 以 DB 为准,因此一旦部署实例的 ny.db 被 e2e 测试串味或误改写成
|
||||||
|
* 127.0.0.1 / mock 地址,登录页就会显示「系统连接信息已配置(127.0.0.1/api)」且连不上真实卷王。
|
||||||
|
* 本工具通过 ny_save_config 双写,一次性把两处都纠正回正确的生产地址。
|
||||||
|
*
|
||||||
|
* 用法:
|
||||||
|
* php tools/fix-config.php [--base-url=https://exam.nxxy335.top] [--api-prefix=/api] [--keep-creds]
|
||||||
|
*
|
||||||
|
* 默认行为:
|
||||||
|
* - base_url 修正为 https://exam.nxxy335.top(可用 --base-url 覆盖)
|
||||||
|
* - api_prefix 修正为 /api
|
||||||
|
* - 账号/密码清空(绝不臆造真实卷王凭据),需在登录页重新输入真实账号
|
||||||
|
* - 加 --keep-creds 则保留现有账号密码不动
|
||||||
|
*
|
||||||
|
* 测试隔离:可用 NY_DATA=/tmp/xxx 指向临时目录,不影响生产库。
|
||||||
|
*/
|
||||||
|
declare(strict_types=1);
|
||||||
|
require __DIR__ . '/../bootstrap.php';
|
||||||
|
|
||||||
|
$args = array_slice($argv, 1);
|
||||||
|
$baseUrl = 'https://exam.nxxy335.top';
|
||||||
|
$apiPrefix = '/api';
|
||||||
|
$keepCreds = false;
|
||||||
|
foreach ($args as $a) {
|
||||||
|
if (strncmp($a, '--base-url=', 11) === 0) {
|
||||||
|
$baseUrl = substr($a, 11);
|
||||||
|
} elseif (strncmp($a, '--api-prefix=', 13) === 0) {
|
||||||
|
$apiPrefix = substr($a, 13);
|
||||||
|
} elseif ($a === '--keep-creds') {
|
||||||
|
$keepCreds = true;
|
||||||
|
} elseif ($a === '--help' || $a === '-h') {
|
||||||
|
echo "用法: php tools/fix-config.php [--base-url=URL] [--api-prefix=/api] [--keep-creds]\n";
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$cfg = ny_load_config();
|
||||||
|
$before = (string)($cfg['base_url'] ?? '');
|
||||||
|
$cfg['base_url'] = rtrim(trim($baseUrl), '/');
|
||||||
|
$cfg['api_prefix'] = '/' . ltrim(trim($apiPrefix), '/');
|
||||||
|
if (!$keepCreds) {
|
||||||
|
$cfg['username'] = '';
|
||||||
|
$cfg['password'] = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$ok = ny_save_config($cfg);
|
||||||
|
if (!$ok) {
|
||||||
|
fwrite(STDERR, "保存失败:DB 与文件均不可写?请检查 " . NY_DATA . " 目录权限。\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$host = (string) parse_url($cfg['base_url'], PHP_URL_HOST);
|
||||||
|
echo "修复前 base_url = " . ($before === '' ? '(空)' : $before) . "\n";
|
||||||
|
echo "修复后 base_url = " . $cfg['base_url'] . "\n";
|
||||||
|
echo "api_prefix = " . $cfg['api_prefix'] . "\n";
|
||||||
|
echo "登录页将显示 = 系统连接信息已配置(" . $host . $cfg['api_prefix'] . ")\n";
|
||||||
|
echo "账号/密码 = " . ($keepCreds ? '保持不变' : '已清空(请在登录页重新输入真实卷王账号)') . "\n";
|
||||||
|
echo "配置来源 = " . ny_config_source() . "\n";
|
||||||
|
echo "OK\n";
|
||||||
+16
-10
@@ -11,10 +11,23 @@ const PHP = process.env.PHP_BIN || 'C:/Users/Zhang/.workbuddy/binaries/php/php.
|
|||||||
const INI = process.env.PHP_INI || '-d extension_dir=C:/Users/Zhang/.workbuddy/binaries/php/ext -d extension=pdo_sqlite';
|
const INI = process.env.PHP_INI || '-d extension_dir=C:/Users/Zhang/.workbuddy/binaries/php/ext -d extension=pdo_sqlite';
|
||||||
|
|
||||||
// 1) 备份 / 准备文件存储模式
|
// 1) 备份 / 准备文件存储模式
|
||||||
const cfgFile = path.join(ROOT, 'data', 'config.json');
|
// 数据目录默认用项目下的 data/,但优先读 NY_DATA 环境变量(bootstrap.php 同样支持):
|
||||||
|
// 测试统一把 NY_DATA 指向临时目录,彻底避免把 mock 配置写进生产库(曾导致生产 base_url 被改成 127.0.0.1)。
|
||||||
|
// Git Bash 风格路径 /c/Users/... 在 Windows 上 path.resolve 会误判成 C:\c\...,需还原成 C:\Users\...;
|
||||||
|
// 规范化后写回 process.env.NY_DATA,让子进程(PHP 内置服务器)也拿到正确的 Windows 绝对路径。
|
||||||
|
function resolveDataDir() {
|
||||||
|
let p = process.env.NY_DATA || path.join(ROOT, 'data');
|
||||||
|
if (process.platform === 'win32' && /^\/[a-zA-Z]\//.test(p)) p = p[1].toUpperCase() + ':' + p.slice(2);
|
||||||
|
p = path.resolve(p);
|
||||||
|
process.env.NY_DATA = p;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
const DATA = resolveDataDir();
|
||||||
|
fs.mkdirSync(DATA, { recursive: true });
|
||||||
|
const cfgFile = path.join(DATA, 'config.json');
|
||||||
const cfgBak = cfgFile + '.bak';
|
const cfgBak = cfgFile + '.bak';
|
||||||
const origCfg = fs.existsSync(cfgFile) ? fs.readFileSync(cfgFile, 'utf8') : null;
|
const origCfg = fs.existsSync(cfgFile) ? fs.readFileSync(cfgFile, 'utf8') : null;
|
||||||
const tokenFile = path.join(ROOT, 'data', 'cache', 'token.json');
|
const tokenFile = path.join(DATA, 'cache', 'token.json');
|
||||||
const tokenBak = tokenFile + '.bak';
|
const tokenBak = tokenFile + '.bak';
|
||||||
const hasToken = fs.existsSync(tokenFile);
|
const hasToken = fs.existsSync(tokenFile);
|
||||||
const tokenSnap = hasToken ? fs.readFileSync(tokenFile, 'utf8') : null;
|
const tokenSnap = hasToken ? fs.readFileSync(tokenFile, 'utf8') : null;
|
||||||
@@ -22,15 +35,11 @@ if (hasToken) fs.renameSync(tokenFile, tokenBak);
|
|||||||
|
|
||||||
// SQLite 优先:若不先移走 data/ny.db,里面的旧配置会盖掉本测试写入的 config.json,
|
// SQLite 优先:若不先移走 data/ny.db,里面的旧配置会盖掉本测试写入的 config.json,
|
||||||
// 导致登录用的是上一次遗留的账号密码(表现为"登录成功但 auth_check=false")。
|
// 导致登录用的是上一次遗留的账号密码(表现为"登录成功但 auth_check=false")。
|
||||||
// 同时清掉登录限流记录,避免上一轮失败把本轮锁住。
|
const dbFile = path.join(DATA, 'ny.db');
|
||||||
const dbFile = path.join(ROOT, 'data', 'ny.db');
|
|
||||||
const dbBak = dbFile + '.bak';
|
const dbBak = dbFile + '.bak';
|
||||||
const hasDb = fs.existsSync(dbFile);
|
const hasDb = fs.existsSync(dbFile);
|
||||||
if (hasDb) { try { fs.renameSync(dbFile, dbBak); } catch (e) {} }
|
if (hasDb) { try { fs.renameSync(dbFile, dbBak); } catch (e) {} }
|
||||||
|
|
||||||
const throttleFile = path.join(ROOT, 'data', 'login_fail.json');
|
|
||||||
if (fs.existsSync(throttleFile)) { try { fs.unlinkSync(throttleFile); } catch (e) {} }
|
|
||||||
|
|
||||||
fs.writeFileSync(cfgFile, JSON.stringify({
|
fs.writeFileSync(cfgFile, JSON.stringify({
|
||||||
base_url: 'http://127.0.0.1:19911',
|
base_url: 'http://127.0.0.1:19911',
|
||||||
api_prefix: '/api',
|
api_prefix: '/api',
|
||||||
@@ -67,9 +76,6 @@ function cleanup() {
|
|||||||
if (fs.existsSync(dbFile)) fs.unlinkSync(dbFile);
|
if (fs.existsSync(dbFile)) fs.unlinkSync(dbFile);
|
||||||
if (hasDb && fs.existsSync(dbBak)) fs.renameSync(dbBak, dbFile);
|
if (hasDb && fs.existsSync(dbBak)) fs.renameSync(dbBak, dbFile);
|
||||||
} catch(e) { console.error('cleanup db failed:', e.message); }
|
} catch(e) { console.error('cleanup db failed:', e.message); }
|
||||||
try {
|
|
||||||
if (fs.existsSync(throttleFile)) fs.unlinkSync(throttleFile);
|
|
||||||
} catch(e) {}
|
|
||||||
}
|
}
|
||||||
process.on('exit', cleanup);
|
process.on('exit', cleanup);
|
||||||
process.on('SIGINT', () => { cleanup(); process.exit(1); });
|
process.on('SIGINT', () => { cleanup(); process.exit(1); });
|
||||||
|
|||||||
+14
-12
@@ -16,9 +16,20 @@ const PHP = process.env.PHP_BIN || 'C:/Users/Zhang/.workbuddy/binaries/php/php.e
|
|||||||
const INI = process.env.PHP_INI || '-d extension_dir=C:/Users/Zhang/.workbuddy/binaries/php/ext -d extension=pdo_sqlite';
|
const INI = process.env.PHP_INI || '-d extension_dir=C:/Users/Zhang/.workbuddy/binaries/php/ext -d extension=pdo_sqlite';
|
||||||
const DEBUG = !!process.env.NY_DEBUG;
|
const DEBUG = !!process.env.NY_DEBUG;
|
||||||
|
|
||||||
const DATA = path.join(ROOT, 'data');
|
// 数据目录优先读 NY_DATA 环境变量(与 bootstrap.php 一致),让测试整体指向临时目录,
|
||||||
|
// 不污染生产库(曾因写 mock 配置把生产 base_url 改成 127.0.0.1)。
|
||||||
|
// Git Bash 风格 /c/Users/... 在 Windows 上 path.resolve 会误判成 C:\c\...,需还原;并把规范化后的
|
||||||
|
// Windows 绝对路径写回 process.env.NY_DATA,让子进程(PHP 内置服务器)也拿到正确路径。
|
||||||
|
function resolveDataDir() {
|
||||||
|
let p = process.env.NY_DATA || path.join(ROOT, 'data');
|
||||||
|
if (process.platform === 'win32' && /^\/[a-zA-Z]\//.test(p)) p = p[1].toUpperCase() + ':' + p.slice(2);
|
||||||
|
p = path.resolve(p);
|
||||||
|
process.env.NY_DATA = p;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
const DATA = resolveDataDir();
|
||||||
|
fs.mkdirSync(DATA, { recursive: true });
|
||||||
const UPLOADS = path.join(DATA, 'uploads');
|
const UPLOADS = path.join(DATA, 'uploads');
|
||||||
const throttleFile = path.join(DATA, 'login_fail.json');
|
|
||||||
|
|
||||||
// ---------- 备份 / 还原运行时数据 ----------
|
// ---------- 备份 / 还原运行时数据 ----------
|
||||||
// ny.db 必须移走:配置是 DB 优先,留着库里的真实配置会盖掉本测试写入的 config.json。
|
// ny.db 必须移走:配置是 DB 优先,留着库里的真实配置会盖掉本测试写入的 config.json。
|
||||||
@@ -41,13 +52,6 @@ for (const f of ['config.json', 'roster.json', 'project_config.json']) {
|
|||||||
catch (e) { console.error(' 备份失败(复制)' + f + ':' + e.message); }
|
catch (e) { console.error(' 备份失败(复制)' + f + ':' + e.message); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 清空登录限流记录:用覆盖写而不是删除(部分环境下删除会被安全策略拦掉)
|
|
||||||
function clearThrottle() {
|
|
||||||
try { fs.writeFileSync(throttleFile, '{}'); } catch (e) {}
|
|
||||||
try { if (fs.existsSync(throttleFile)) fs.unlinkSync(throttleFile); } catch (e) {}
|
|
||||||
}
|
|
||||||
clearThrottle();
|
|
||||||
|
|
||||||
const cfgFile = path.join(DATA, 'config.json');
|
const cfgFile = path.join(DATA, 'config.json');
|
||||||
fs.writeFileSync(cfgFile, JSON.stringify({
|
fs.writeFileSync(cfgFile, JSON.stringify({
|
||||||
base_url: 'http://127.0.0.1:' + MOCK_PORT,
|
base_url: 'http://127.0.0.1:' + MOCK_PORT,
|
||||||
@@ -81,7 +85,6 @@ function cleanup() {
|
|||||||
const p = path.join(DATA, f);
|
const p = path.join(DATA, f);
|
||||||
try { if (fs.existsSync(p)) fs.unlinkSync(p); } catch (e) {}
|
try { if (fs.existsSync(p)) fs.unlinkSync(p); } catch (e) {}
|
||||||
}
|
}
|
||||||
clearThrottle();
|
|
||||||
// 统一用「覆盖写回 + 尽力删除备份」的方式还原,避免删除被拦导致还原失败
|
// 统一用「覆盖写回 + 尽力删除备份」的方式还原,避免删除被拦导致还原失败
|
||||||
for (const [p, bak] of moved.concat(copied)) {
|
for (const [p, bak] of moved.concat(copied)) {
|
||||||
try { if (fs.existsSync(bak)) fs.copyFileSync(bak, p); } catch (e) {}
|
try { if (fs.existsSync(bak)) fs.copyFileSync(bak, p); } catch (e) {}
|
||||||
@@ -199,9 +202,8 @@ function chk(name, cond, extra) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登录(失败会触发 5 次锁定,所以每次尝试前清掉限流记录)
|
// 登录(最多重试 3 次)
|
||||||
for (let i = 0; i < 3 && CSRF === ''; i++) {
|
for (let i = 0; i < 3 && CSRF === ''; i++) {
|
||||||
clearThrottle(); // 上一次尝试若失败会累计限流,这里先清零
|
|
||||||
const lg = await formPost('/login.php', { username: 'mock', password: 'surveyking' });
|
const lg = await formPost('/login.php', { username: 'mock', password: 'surveyking' });
|
||||||
const ac = await get('/api.php?action=auth_check');
|
const ac = await get('/api.php?action=auth_check');
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
|
|||||||
Reference in New Issue
Block a user