feat(auth): 增加管理员登录的速率限制和错误处理

This commit is contained in:
Jeff Gu committed 2026-04-14 17:01:07 +08:00
1 parent 8336649477
commit 3dc1fed1c1
5 files changed
+460 -3

No files matched your search

+126
View File
@@ -5,10 +5,14 @@ import { z } from "zod";
import { CommonErrorResponse, CommonSuccessfulResponse } from "../model";
import { getSystemConfig, isTelegramSecurityEnabled } from "../system-config";
import {
AdminRateLimitError,
clearAdminRateLimitBucket,
clearAdminSessionCookie,
consumeAdminRateLimit,
createAdminLoginChallenge,
createAdminSession,
deleteAdminLoginChallenge,
getRequestMetadata,
getAdminSessionTokenFromRequest,
invalidateAdminSession,
setAdminSessionCookie,
@@ -17,6 +21,44 @@ import {
verifyAdminLoginChallenge,
} from "./auth_shared";
const ADMIN_LOGIN_START_ATTEMPT_POLICY = {
category: "admin-login-start:attempt",
maxAttempts: 6,
windowMs: 10 * 60 * 1000,
blockDurationMs: 15 * 60 * 1000,
message: "管理员登录请求过于频繁,请稍后再试",
} as const;
const ADMIN_LOGIN_START_FAILURE_POLICY = {
category: "admin-login-start:failure",
maxAttempts: 5,
windowMs: 30 * 60 * 1000,
blockDurationMs: 30 * 60 * 1000,
message: "管理员登录失败次数过多,请稍后再试",
} as const;
const ADMIN_LOGIN_VERIFY_ATTEMPT_POLICY = {
category: "admin-login-verify:attempt",
maxAttempts: 8,
windowMs: 10 * 60 * 1000,
blockDurationMs: 15 * 60 * 1000,
message: "验证码验证请求过于频繁,请稍后再试",
} as const;
const ADMIN_LOGIN_VERIFY_FAILURE_POLICY = {
category: "admin-login-verify:failure",
maxAttempts: 6,
windowMs: 30 * 60 * 1000,
blockDurationMs: 30 * 60 * 1000,
message: "验证码验证失败次数过多,请稍后再试",
} as const;
const buildRateLimitedResponse = (
c: Context<HonoCustomType>,
message: string,
retryAfterSeconds: number
) => {
c.header("Retry-After", String(retryAfterSeconds));
return c.text(message, 429);
};
const loginStartRequestSchema = z.object({
token: z.string().trim().min(1, "管理员令牌不能为空"),
});
@@ -81,11 +123,32 @@ export class AdminLoginStartEndpoint extends OpenAPIRoute {
}
const body = parsedBody.data;
const metadata = getRequestMetadata(c);
const rateLimitBucketId = metadata.clientIp || "unknown";
const startAttemptLimit = await consumeAdminRateLimit(
c,
ADMIN_LOGIN_START_ATTEMPT_POLICY,
rateLimitBucketId
);
if (!startAttemptLimit.ok) {
return buildRateLimitedResponse(
c,
startAttemptLimit.message,
startAttemptLimit.retryAfterSeconds
);
}
const systemConfig = await getSystemConfig(c);
const securityConfig = systemConfig.adminSecurity;
const securityEnabled = isTelegramSecurityEnabled(securityConfig);
if (!c.env.ADMIN_TOKEN || body.token !== c.env.ADMIN_TOKEN) {
const failureLimit = await consumeAdminRateLimit(
c,
ADMIN_LOGIN_START_FAILURE_POLICY,
rateLimitBucketId
);
if (securityEnabled) {
await sendAdminLoginResultNotification(
securityConfig,
@@ -97,9 +160,23 @@ export class AdminLoginStartEndpoint extends OpenAPIRoute {
});
}
if (!failureLimit.ok) {
return buildRateLimitedResponse(
c,
failureLimit.message,
failureLimit.retryAfterSeconds
);
}
return c.text("管理员令牌无效", 401);
}
await clearAdminRateLimitBucket(
c,
ADMIN_LOGIN_START_FAILURE_POLICY.category,
rateLimitBucketId
);
if (!securityEnabled) {
return buildDirectLoginResponse(c);
}
@@ -116,6 +193,14 @@ export class AdminLoginStartEndpoint extends OpenAPIRoute {
} catch (error) {
await deleteAdminLoginChallenge(c, challenge.challengeId);
if (error instanceof AdminRateLimitError) {
return buildRateLimitedResponse(
c,
error.message,
error.retryAfterSeconds
);
}
return c.text(
`Telegram 发送验证码失败:${error instanceof Error ? error.message : "unknown error"}`,
500
@@ -164,6 +249,21 @@ export class AdminLoginVerifyEndpoint extends OpenAPIRoute {
}
const body = parsedBody.data;
const metadata = getRequestMetadata(c);
const rateLimitBucketId = metadata.clientIp || "unknown";
const verifyAttemptLimit = await consumeAdminRateLimit(
c,
ADMIN_LOGIN_VERIFY_ATTEMPT_POLICY,
rateLimitBucketId
);
if (!verifyAttemptLimit.ok) {
return buildRateLimitedResponse(
c,
verifyAttemptLimit.message,
verifyAttemptLimit.retryAfterSeconds
);
}
const systemConfig = await getSystemConfig(c);
const securityConfig = systemConfig.adminSecurity;
@@ -178,6 +278,11 @@ export class AdminLoginVerifyEndpoint extends OpenAPIRoute {
);
if (!result.ok) {
const failureLimit = await consumeAdminRateLimit(
c,
ADMIN_LOGIN_VERIFY_FAILURE_POLICY,
rateLimitBucketId
);
await sendAdminLoginResultNotification(
securityConfig,
c,
@@ -187,9 +292,30 @@ export class AdminLoginVerifyEndpoint extends OpenAPIRoute {
console.error("Failed to send Telegram login failure notification:", error);
});
if (!failureLimit.ok) {
return buildRateLimitedResponse(
c,
failureLimit.message,
failureLimit.retryAfterSeconds
);
}
return c.text(result.reason || "验证码验证失败", 401);
}
await Promise.all([
clearAdminRateLimitBucket(
c,
ADMIN_LOGIN_START_FAILURE_POLICY.category,
rateLimitBucketId
),
clearAdminRateLimitBucket(
c,
ADMIN_LOGIN_VERIFY_FAILURE_POLICY.category,
rateLimitBucketId
),
]);
const session = await createAdminSession(c);
setAdminSessionCookie(c, session.sessionToken, session.expiresAt);
+275 -1
View File
@@ -5,8 +5,51 @@ const ADMIN_SESSION_COOKIE_NAME = "oaw_admin_session";
const ADMIN_SESSION_TTL_MS = 12 * 60 * 60 * 1000;
const ADMIN_LOGIN_CHALLENGE_TTL_MS = 5 * 60 * 1000;
const ADMIN_LOGIN_CHALLENGE_MAX_ATTEMPTS = 5;
const ADMIN_RATE_LIMIT_RETENTION_MS = 24 * 60 * 60 * 1000;
const DEFAULT_SYSTEM_TIMEZONE = "Asia/Shanghai";
const LOCAL_DEV_HOSTNAMES = new Set(["0.0.0.0", "127.0.0.1", "::1", "localhost"]);
const ADMIN_TELEGRAM_CODE_NOTIFY_PER_IP_POLICY = {
category: "admin-telegram-code:ip",
maxAttempts: 3,
windowMs: 10 * 60 * 1000,
blockDurationMs: 10 * 60 * 1000,
message: "验证码发送过于频繁,请稍后再试",
} as const;
const ADMIN_TELEGRAM_CODE_NOTIFY_GLOBAL_POLICY = {
category: "admin-telegram-code:global",
maxAttempts: 12,
windowMs: 10 * 60 * 1000,
blockDurationMs: 10 * 60 * 1000,
message: "验证码发送过于频繁,请稍后再试",
} as const;
const ADMIN_TELEGRAM_RESULT_FAILURE_PER_IP_POLICY = {
category: "admin-telegram-result-failure:ip",
maxAttempts: 2,
windowMs: 10 * 60 * 1000,
blockDurationMs: 10 * 60 * 1000,
message: "登录失败通知发送过于频繁,已自动静默",
} as const;
const ADMIN_TELEGRAM_RESULT_FAILURE_GLOBAL_POLICY = {
category: "admin-telegram-result-failure:global",
maxAttempts: 20,
windowMs: 10 * 60 * 1000,
blockDurationMs: 10 * 60 * 1000,
message: "登录失败通知发送过于频繁,已自动静默",
} as const;
const ADMIN_TELEGRAM_RESULT_SUCCESS_PER_IP_POLICY = {
category: "admin-telegram-result-success:ip",
maxAttempts: 3,
windowMs: 30 * 60 * 1000,
blockDurationMs: 30 * 60 * 1000,
message: "登录成功通知发送过于频繁,已自动静默",
} as const;
const ADMIN_TELEGRAM_RESULT_SUCCESS_GLOBAL_POLICY = {
category: "admin-telegram-result-success:global",
maxAttempts: 20,
windowMs: 30 * 60 * 1000,
blockDurationMs: 30 * 60 * 1000,
message: "登录成功通知发送过于频繁,已自动静默",
} as const;
type LoginRequestMetadata = {
clientIp: string;
@@ -17,6 +60,38 @@ type LoginRequestMetadata = {
timezone: string;
};
type StoredAdminRateLimitRow = {
attempts: number | null;
blocked_until: string | null;
bucket_id: string;
bucket_key: string;
category: string;
last_event_at: string;
window_started_at: string;
};
type AdminRateLimitPolicy = {
blockDurationMs: number;
category: string;
maxAttempts: number;
message: string;
windowMs: number;
};
type AdminRateLimitResult =
| { ok: true }
| { ok: false; message: string; retryAfterSeconds: number };
export class AdminRateLimitError extends Error {
readonly retryAfterSeconds: number;
constructor(message: string, retryAfterSeconds: number) {
super(message);
this.name = "AdminRateLimitError";
this.retryAfterSeconds = retryAfterSeconds;
}
}
const firstNonEmpty = (...values: Array<string | null | undefined>): string => {
for (const value of values) {
if (typeof value === "string" && value.trim().length > 0) {
@@ -45,7 +120,7 @@ const formatTimestampInTimezone = (date: Date, timezone: string): string => {
return `${values.year}-${values.month}-${values.day} ${values.hour}:${values.minute}:${values.second}`;
};
const getRequestMetadata = (c: Context<HonoCustomType>): LoginRequestMetadata => {
export const getRequestMetadata = (c: Context<HonoCustomType>): LoginRequestMetadata => {
const requestCf = (c.req.raw.cf || {}) as Partial<IncomingRequestCfProperties<unknown>>;
return {
@@ -93,8 +168,65 @@ const toSha256Hex = async (value: string): Promise<string> => {
.join("");
};
const getAdminRateLimitBucketId = (metadata: LoginRequestMetadata): string => {
return metadata.clientIp || "unknown";
};
const buildAdminRateLimitBucketKey = (
category: string,
bucketId: string
): string => {
return `${category}:${bucketId}`;
};
const toRetryAfterSeconds = (retryAfterMs: number): number => {
return Math.max(1, Math.ceil(retryAfterMs / 1000));
};
const upsertAdminRateLimit = async (
c: Context<HonoCustomType>,
row: {
attempts: number;
blockedUntil: string | null;
bucketId: string;
bucketKey: string;
category: string;
lastEventAt: string;
windowStartedAt: string;
}
) => {
await c.env.DB.prepare(
`INSERT INTO admin_rate_limit (
bucket_key,
category,
bucket_id,
window_started_at,
attempts,
blocked_until,
last_event_at
) VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(bucket_key) DO UPDATE SET
category = excluded.category,
bucket_id = excluded.bucket_id,
window_started_at = excluded.window_started_at,
attempts = excluded.attempts,
blocked_until = excluded.blocked_until,
last_event_at = excluded.last_event_at,
updated_at = datetime('now')`
).bind(
row.bucketKey,
row.category,
row.bucketId,
row.windowStartedAt,
row.attempts,
row.blockedUntil,
row.lastEventAt
).run();
};
const cleanupExpiredArtifacts = async (c: Context<HonoCustomType>) => {
const now = new Date().toISOString();
const rateLimitRetentionCutoff = new Date(Date.now() - ADMIN_RATE_LIMIT_RETENTION_MS).toISOString();
await Promise.all([
c.env.DB.prepare(
@@ -103,9 +235,109 @@ const cleanupExpiredArtifacts = async (c: Context<HonoCustomType>) => {
c.env.DB.prepare(
`DELETE FROM admin_session WHERE expires_at <= ?`
).bind(now).run(),
c.env.DB.prepare(
`DELETE FROM admin_rate_limit WHERE last_event_at <= ?`
).bind(rateLimitRetentionCutoff).run(),
]);
};
export const consumeAdminRateLimit = async (
c: Context<HonoCustomType>,
policy: AdminRateLimitPolicy,
bucketId: string
): Promise<AdminRateLimitResult> => {
await cleanupExpiredArtifacts(c);
const now = new Date();
const nowMs = now.getTime();
const nowIso = now.toISOString();
const bucketKey = buildAdminRateLimitBucketKey(policy.category, bucketId);
const existing = await c.env.DB.prepare(
`SELECT
bucket_key,
category,
bucket_id,
window_started_at,
attempts,
blocked_until,
last_event_at
FROM admin_rate_limit
WHERE bucket_key = ?`
).bind(bucketKey).first<StoredAdminRateLimitRow>();
const blockedUntilMs = existing?.blocked_until ? Date.parse(existing.blocked_until) : Number.NaN;
if (existing?.blocked_until && Number.isFinite(blockedUntilMs) && blockedUntilMs > nowMs) {
return {
ok: false,
message: policy.message,
retryAfterSeconds: toRetryAfterSeconds(blockedUntilMs - nowMs),
};
}
const windowStartedAtMs = existing?.window_started_at
? Date.parse(existing.window_started_at)
: Number.NaN;
const isSameWindow = Number.isFinite(windowStartedAtMs)
&& nowMs - windowStartedAtMs < policy.windowMs;
const windowStartedAt = isSameWindow && existing?.window_started_at
? existing.window_started_at
: nowIso;
const previousAttempts = isSameWindow ? existing?.attempts || 0 : 0;
const nextAttempts = previousAttempts + 1;
if (nextAttempts > policy.maxAttempts) {
const blockedUntil = new Date(nowMs + policy.blockDurationMs).toISOString();
await upsertAdminRateLimit(c, {
attempts: nextAttempts,
blockedUntil,
bucketId,
bucketKey,
category: policy.category,
lastEventAt: nowIso,
windowStartedAt,
});
return {
ok: false,
message: policy.message,
retryAfterSeconds: toRetryAfterSeconds(policy.blockDurationMs),
};
}
await upsertAdminRateLimit(c, {
attempts: nextAttempts,
blockedUntil: null,
bucketId,
bucketKey,
category: policy.category,
lastEventAt: nowIso,
windowStartedAt,
});
return { ok: true };
};
export const clearAdminRateLimitBucket = async (
c: Context<HonoCustomType>,
category: string,
bucketId: string
): Promise<void> => {
await c.env.DB.prepare(
`DELETE FROM admin_rate_limit WHERE bucket_key = ?`
).bind(buildAdminRateLimitBucketKey(category, bucketId)).run();
};
const throwIfAdminRateLimited = async (
c: Context<HonoCustomType>,
policy: AdminRateLimitPolicy,
bucketId: string
): Promise<void> => {
const result = await consumeAdminRateLimit(c, policy, bucketId);
if (!result.ok) {
throw new AdminRateLimitError(result.message, result.retryAfterSeconds);
}
};
const sendTelegramMessage = async (
securityConfig: AdminSecurityConfig,
text: string
@@ -340,6 +572,19 @@ export const verifyAdminLoginChallenge = async (
};
}
const metadata = getRequestMetadata(c);
if (
challenge.request_ip
&& metadata.clientIp
&& challenge.request_ip !== metadata.clientIp
) {
await deleteAdminLoginChallenge(c, challengeId);
return {
ok: false,
reason: "登录来源已变化,请重新获取验证码",
};
}
if (Date.parse(challenge.expires_at) <= Date.now()) {
await deleteAdminLoginChallenge(c, challengeId);
return {
@@ -398,6 +643,18 @@ export const sendAdminLoginCodeNotification = async (
const occurredAt = new Date();
const expiresDate = new Date(expiresAt);
const clientTimezone = metadata.timezone || DEFAULT_SYSTEM_TIMEZONE;
const bucketId = getAdminRateLimitBucketId(metadata);
await throwIfAdminRateLimited(
c,
ADMIN_TELEGRAM_CODE_NOTIFY_PER_IP_POLICY,
bucketId
);
await throwIfAdminRateLimited(
c,
ADMIN_TELEGRAM_CODE_NOTIFY_GLOBAL_POLICY,
"global"
);
await sendTelegramMessage(
securityConfig,
@@ -417,6 +674,23 @@ export const sendAdminLoginResultNotification = async (
): Promise<void> => {
const metadata = getRequestMetadata(c);
const occurredAt = new Date();
const bucketId = getAdminRateLimitBucketId(metadata);
const policies = status === "success"
? [ADMIN_TELEGRAM_RESULT_SUCCESS_PER_IP_POLICY, ADMIN_TELEGRAM_RESULT_SUCCESS_GLOBAL_POLICY]
: [ADMIN_TELEGRAM_RESULT_FAILURE_PER_IP_POLICY, ADMIN_TELEGRAM_RESULT_FAILURE_GLOBAL_POLICY];
for (const policy of policies) {
const result = await consumeAdminRateLimit(
c,
policy,
policy.category.endsWith(":global") ? "global" : bucketId
);
if (!result.ok) {
console.warn(`Skipped Telegram login ${status} notification due to rate limit: ${policy.category}`);
return;
}
}
await sendTelegramMessage(
securityConfig,
+15
View File
@@ -46,10 +46,25 @@ CREATE TABLE IF NOT EXISTS admin_session (
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS admin_rate_limit (
bucket_key TEXT PRIMARY KEY,
category TEXT NOT NULL,
bucket_id TEXT NOT NULL,
window_started_at TEXT NOT NULL,
attempts INTEGER DEFAULT 0,
blocked_until TEXT,
last_event_at TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_admin_login_challenge_expires_at
ON admin_login_challenge (expires_at);
CREATE INDEX IF NOT EXISTS idx_admin_session_expires_at
ON admin_session (expires_at);
CREATE INDEX IF NOT EXISTS idx_admin_rate_limit_category_bucket
ON admin_rate_limit (category, bucket_id);
CREATE INDEX IF NOT EXISTS idx_admin_rate_limit_blocked_until
ON admin_rate_limit (blocked_until);
`
let dbReadyPromise: Promise<void> | null = null;
+43 -2
View File
@@ -59,6 +59,19 @@ do_get() { curl -s -w "\n%{http_code}" "$@"; }
do_post() { curl -s -w "\n%{http_code}" -X POST "$@"; }
reset_mock_inspection() { curl -s -X POST "$MOCK_API/__reset" > /dev/null; }
get_mock_inspection() { curl -sG "$MOCK_API/__inspect" --data-urlencode "key=$1"; }
set_system_config_disabled() {
"${DB_EXEC[@]}" "INSERT OR REPLACE INTO settings (key, value) VALUES ('SYSTEM_CONFIG', '{\"displayDecimals\":6,\"adminSecurity\":{\"enabled\":false,\"telegramBotToken\":\"\",\"telegramChatId\":\"\",\"verifiedFingerprint\":\"\",\"verifiedAt\":null},\"apiDocs\":{\"enabled\":true}}')" > /dev/null 2>&1
}
set_system_config_with_telegram() {
local bot_token="$1" chat_id="$2" fingerprint
fingerprint=$(node -e "const source = process.argv[1] + '::' + process.argv[2]; let hash = 2166136261; for (let index = 0; index < source.length; index += 1) { hash ^= source.charCodeAt(index); hash = Math.imul(hash, 16777619); } process.stdout.write(source ? 'tgv1-' + ((hash >>> 0).toString(16)) : '');" "$bot_token" "$chat_id")
"${DB_EXEC[@]}" "INSERT OR REPLACE INTO settings (key, value) VALUES ('SYSTEM_CONFIG', '{\"displayDecimals\":6,\"adminSecurity\":{\"enabled\":true,\"telegramBotToken\":\"$bot_token\",\"telegramChatId\":\"$chat_id\",\"verifiedFingerprint\":\"$fingerprint\",\"verifiedAt\":\"2026-04-14T00:00:00.000Z\"},\"apiDocs\":{\"enabled\":true}}')" > /dev/null 2>&1
}
seed_blocked_rate_limit() {
local category="$1" bucket_id="$2" attempts="$3" blocked_until="$4" now_iso
now_iso=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
"${DB_EXEC[@]}" "INSERT OR REPLACE INTO admin_rate_limit (bucket_key, category, bucket_id, window_started_at, attempts, blocked_until, last_event_at) VALUES ('$category:$bucket_id', '$category', '$bucket_id', '$now_iso', $attempts, '$blocked_until', '$now_iso')" > /dev/null 2>&1
}
if [[ -f ".dev.vars" ]]; then
set -a
@@ -67,6 +80,8 @@ if [[ -f ".dev.vars" ]]; then
set +a
fi
LOGIN_PAYLOAD=$(printf '{"token":"%s"}' "$ADMIN_TOKEN")
echo ""
echo "========================================="
echo " E2E Tests with Mock Upstream"
@@ -241,9 +256,8 @@ assert_http "quota exceeded → 402" "402" "Quota exceeded" "$R"
echo ""
echo "--- Admin session cookie ---"
"${DB_EXEC[@]}" "INSERT OR REPLACE INTO settings (key, value) VALUES ('SYSTEM_CONFIG', '{\"displayDecimals\":6,\"adminSecurity\":{\"enabled\":false,\"telegramBotToken\":\"\",\"telegramChatId\":\"\",\"verifiedFingerprint\":\"\",\"verifiedAt\":null},\"apiDocs\":{\"enabled\":true}}')" > /dev/null 2>&1
set_system_config_disabled
COOKIE_JAR=$(mktemp)
LOGIN_PAYLOAD=$(printf '{"token":"%s"}' "$ADMIN_TOKEN")
R=$(do_post "$API/api/admin/auth/login" -H "Content-Type: application/json" -c "$COOKIE_JAR" -d "$LOGIN_PAYLOAD")
assert_http "admin login → 200" "200" "\"requiresVerification\":false" "$R"
@@ -261,6 +275,32 @@ assert_http "admin cookie invalid after logout" "401" "Unauthorized" "$R"
rm -f "$COOKIE_JAR"
# ---- Admin anti-bruteforce ----
echo ""
echo "--- Admin anti-bruteforce ---"
set_system_config_disabled
LIMIT_IP="203.0.113.70"
"${DB_EXEC[@]}" "DELETE FROM admin_rate_limit" > /dev/null 2>&1
for attempt in 1 2 3 4 5; do
R=$(do_post "$API/api/admin/auth/login" -H "Content-Type: application/json" -H "CF-Connecting-IP: $LIMIT_IP" -d '{"token":"bad-token"}')
assert_http "admin invalid token attempt $attempt → 401" "401" "管理员令牌无效" "$R"
done
R=$(do_post "$API/api/admin/auth/login" -H "Content-Type: application/json" -H "CF-Connecting-IP: $LIMIT_IP" -d '{"token":"bad-token"}')
assert_http "admin invalid token freeze → 429" "429" "管理员登录失败次数过多" "$R"
# ---- Telegram notification throttle ----
echo ""
echo "--- Telegram notification throttle ---"
THROTTLE_IP="203.0.113.71"
THROTTLE_UNTIL=$(date -u -d "+15 minutes" +"%Y-%m-%dT%H:%M:%SZ")
set_system_config_with_telegram "dummy-bot-token" "123456"
"${DB_EXEC[@]}" "DELETE FROM admin_rate_limit" > /dev/null 2>&1
seed_blocked_rate_limit "admin-telegram-code:ip" "$THROTTLE_IP" 99 "$THROTTLE_UNTIL"
R=$(do_post "$API/api/admin/auth/login" -H "Content-Type: application/json" -H "CF-Connecting-IP: $THROTTLE_IP" -d "$LOGIN_PAYLOAD")
assert_http "telegram code notification throttle → 429" "429" "验证码发送过于频繁" "$R"
# ---- Error recovery ----
echo ""
echo "--- Error recovery ---"
@@ -284,6 +324,7 @@ done
for key in sk-mock-all sk-mock-openai sk-mock-quota; do
"${DB_EXEC[@]}" "DELETE FROM api_token WHERE key = '$key'" > /dev/null 2>&1
done
"${DB_EXEC[@]}" "DELETE FROM admin_rate_limit; DELETE FROM admin_login_challenge; DELETE FROM admin_session; DELETE FROM settings WHERE key = 'SYSTEM_CONFIG'" > /dev/null 2>&1
echo "Cleanup done."
exit $FAIL
+1
View File
@@ -12,6 +12,7 @@ echo "Setting up test channels and tokens..."
# Warm up the worker so schema migrations run against the same local D1 database.
curl -s "$API_URL/api/admin/channel" > /dev/null 2>&1 || true
"${DB_EXEC[@]}" "DELETE FROM admin_rate_limit; DELETE FROM admin_login_challenge; DELETE FROM admin_session; DELETE FROM settings WHERE key = 'SYSTEM_CONFIG'" > /dev/null 2>&1 || true
# Channel: openai type
"${DB_EXEC[@]}" "INSERT OR REPLACE INTO channel_config (key, value) VALUES ('test-openai', '{\"name\":\"test-openai\",\"type\":\"openai\",\"endpoint\":\"$MOCK\",\"api_key\":\"mock-key\",\"deployment_mapper\":{\"gpt-4o\":\"gpt-4o\",\"gpt-4\":\"gpt-4\"}}')"