Files
OJ2/apps/api/src/config.ts
yuetsh dd6a6a04eb
Some checks failed
Deploy / deploy (push) Has been cancelled
refactor(密码): 删掉 PASSWORD_HASH_UPGRADE,无条件写 argon2
上一个 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>
2026-08-25 23:50:55 -06:00

87 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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",
}