Some checks failed
Deploy / deploy (push) Has been cancelled
上一个 commit 把五个写入点收进 hashPassword,顺手把开关默认值翻成了 true 并 留下 false 分支当退路。这个退路是假的,删掉。 **开关只能拦住将来,修不了已经发生的。** 等真到了要把旧站拉回来的那天,活跃 账号早就都被登录/重置密码升成 argon2 了,这时候设 `=false` 一个也救不回来。 正经退路是切换手册「万一已经改坏了」那节的脚本 —— 拿 `raw_password` 重算 Django 的 `make_password`,能修已经变成 argon2 的账号。留着一个永远轮不到它 上场的开关,只是给后来人多一件要读懂的东西。 删掉的:`config.passwordHashUpgrade`、`hashDjangoPbkdf2` 和它那套 Django 参数 (1200000 迭代 / 22 位 salt / RANDOM_STRING_CHARS)、两个 compose 里的 `PASSWORD_HASH_UPGRADE` env。auth.ts 的登录升级变成无条件。 保留的: - `hashPassword()` —— 虽然现在只是一行 argon2,但「只有一个地方写密码」正是上 一个 commit 的重点。五处各写各的才是当初那个 bug 的根。 - `verifyPassword` 的 **pbkdf2 分支** —— 生产库 1710 个账号全是 Django 写的 pbkdf2(迭代次数 120000~1200000),只会在各自下次登录时才迁移。删了就是 全站登不上。代码注释里写死了这句话。 - 切换手册里的 `raw_password` 恢复脚本,以及回滚流程里新加的「密码要额外处理」 那一步。 ## 验证 改完重跑了一遍关键路径(临时造一个 Django 生成的 120000 迭代老哈希): - 存量账号登录 200 → 哈希变成 argon2 → 再登 200 → 错密码 401 - 后台重置密码 → argon2 → 新密码能登录 - 注册 → argon2 → 能登录 tsc(apps/api) 0 error、check:routes 168 条无遮蔽、vue-tsc 0 error、build 通过、 两个 compose `config -q` 通过。测试用户已删干净。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
87 lines
4.1 KiB
TypeScript
87 lines
4.1 KiB
TypeScript
import { randomBytes } from "node:crypto"
|
||
import { readFileSync } from "node:fs"
|
||
import { isAbsolute, resolve } from "node:path"
|
||
|
||
import { isCompiled, pathBase } from "./runtime"
|
||
|
||
/**
|
||
* Bun 只自动加载「当前工作目录」下的 .env。而本应用的启动方式(`bun run --filter '@oj2/api' dev`)
|
||
* 会把 cwd 切到 apps/api/,于是仓库根的 .env 读不到 —— 而 .env.example 恰恰教人写在根目录。
|
||
* 这里显式补读仓库根的 .env,让文档指引真正生效,且不管从哪个目录启动都一致。
|
||
*
|
||
* 只填充尚未设置的键:真实环境变量与 cwd 下的 .env 优先级更高,不被覆盖。
|
||
*
|
||
* 编译成单二进制后不做这件事:生产靠 compose 注入环境变量,而 `import.meta.dir` 在
|
||
* 二进制里是 `/$bunfs/root`,往上三级会去读 `/.env` —— 读到什么都是意外。
|
||
*/
|
||
function loadRepoRootEnv() {
|
||
if (isCompiled) return
|
||
try {
|
||
const text = readFileSync(resolve(pathBase, ".env"), "utf8")
|
||
for (const line of text.split("\n")) {
|
||
const trimmed = line.trim()
|
||
if (!trimmed || trimmed.startsWith("#")) continue
|
||
const eq = trimmed.indexOf("=")
|
||
if (eq <= 0) continue
|
||
const key = trimmed.slice(0, eq).trim()
|
||
if (process.env[key] !== undefined) continue
|
||
process.env[key] = trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, "")
|
||
}
|
||
} catch {
|
||
// 根目录没有 .env 是正常情况(例如生产用真实环境变量注入),静默跳过
|
||
}
|
||
}
|
||
|
||
loadRepoRootEnv()
|
||
|
||
/**
|
||
* 判题机 token。对齐旧后端 `options/options.py:93`:
|
||
* token = os.environ.get("JUDGE_SERVER_TOKEN"); return token if token else rand_str()
|
||
* env 缺失时生成随机值 fail-safe —— 宁可判题机连不上(启动时有明显告警),
|
||
* 也不要在仓库里写死一个人人都知道的弱默认值。
|
||
*/
|
||
/**
|
||
* 相对路径按 `pathBase` 解析(开发时是仓库根,编译后是 cwd),基准的取舍见 runtime.ts。
|
||
*
|
||
* 生产环境**应当**用绝对路径的环境变量把这些目录显式指定掉,相对路径只是开发便利。
|
||
*/
|
||
function repoPath(value: string) {
|
||
return isAbsolute(value) ? value : resolve(pathBase, value)
|
||
}
|
||
|
||
function judgeServerToken() {
|
||
const fromEnv = process.env.JUDGE_SERVER_TOKEN
|
||
if (fromEnv) return fromEnv
|
||
console.warn(
|
||
"[config] JUDGE_SERVER_TOKEN 未设置,已生成一次性随机 token。" +
|
||
"判题机将无法通过鉴权,本地开发请在 .env 里设置 JUDGE_SERVER_TOKEN," +
|
||
"并让 docker/compose.dev.yml 的 OJ2_JUDGE_TOKEN 取同一个值。",
|
||
)
|
||
return randomBytes(32).toString("hex")
|
||
}
|
||
|
||
export const config = {
|
||
port: Number(process.env.PORT ?? 3000),
|
||
redisUrl: process.env.REDIS_URL ?? "redis://localhost:6380",
|
||
sessionCookie: "oj2_session",
|
||
sessionTtlSeconds: Number(process.env.SESSION_TTL_SECONDS ?? 7 * 24 * 60 * 60),
|
||
secureCookies: process.env.COOKIE_SECURE === "true",
|
||
judgeServerUrl: process.env.JUDGE_SERVER_URL ?? "http://localhost:8081",
|
||
judgeServerToken: judgeServerToken(),
|
||
judgeConcurrency: Number(process.env.JUDGE_CONCURRENCY ?? 2),
|
||
avatarDirectory: repoPath(process.env.AVATAR_DIRECTORY ?? "data/avatar"),
|
||
// 判题沙箱把这个目录挂成只读的 /test_case,两边必须指同一处
|
||
testCaseDirectory: repoPath(process.env.TEST_CASE_DIRECTORY ?? "data/test_case"),
|
||
uploadDirectory: repoPath(process.env.UPLOAD_DIRECTORY ?? "data/upload"),
|
||
// 一言数据集(hitokoto.cn 官方导出),和旧后端读同一份:容器里是 /data/hitokoto。
|
||
// 本机 dev 默认路径下没有这份数据,读不到就回落到内置的几条,不影响启动。
|
||
hitokotoDirectory: repoPath(process.env.HITOKOTO_DIRECTORY ?? "data/hitokoto"),
|
||
uploadUriPrefix: process.env.UPLOAD_URI_PREFIX ?? "/public/upload",
|
||
avatarUriPrefix: process.env.AVATAR_URI_PREFIX ?? "/public/avatar",
|
||
aiBaseUrl: process.env.AI_BASE_URL ?? "https://api.deepseek.com",
|
||
aiKey: process.env.AI_KEY ?? "",
|
||
aiModel: process.env.AI_MODEL ?? "deepseek-v4-flash",
|
||
ruffPath: process.env.RUFF_PATH ?? "ruff",
|
||
clangFormatPath: process.env.CLANG_FORMAT_PATH ?? "clang-format",
|
||
}
|