Deploy / deploy (push) Has been cancelled
原来只有 `apps/web` 在 Prettier 下(配置在 `apps/web/.prettierrc.toml`、脚本在 web 的 package.json),后端和契约从来没格式化过 —— 手写在 100 列上下,`db/schema.ts` 还是 drizzle-kit pull 留下的 tab 缩进。两套口径分叉久了,跨端改一处就得记着「这边 什么风格」。 - 配置搬到根目录 `.prettierrc.toml`,内容不变(`semi=false`,其余全默认, printWidth 80 —— 和前端已有的格式一致,不另立一套宽度); - 脚本统一成根目录 `bun run fmt`,覆盖 `apps/*/src`、`apps/web/tests` 和两个构建 配置;web 自己那份 `fmt` 和重复的 prettier 依赖删掉; - `.prettierignore` 挡掉两类不该碰的:drizzle-kit 生成的 `src/db/meta/` 结构快照 (它是 db:generate 的比对输入,只该由 drizzle-kit 写)、unplugin 每次 dev 都会 重写的 `auto-imports.d.ts` / `components.d.ts`; - 全量跑了一遍。纯格式,无行为改动:api typecheck / check:routes / check:ast、 前端 type-check 全过,起 api 打了接口确认正常。前端这 39 个文件的小改动是 prettier 版本漂移(类型断言的换行口径变了),不是新配置带来的。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
93 lines
3.2 KiB
TypeScript
93 lines
3.2 KiB
TypeScript
import { pbkdf2, timingSafeEqual } from "node:crypto"
|
||
import { promisify } from "node:util"
|
||
|
||
const pbkdf2Async = promisify(pbkdf2)
|
||
|
||
async function verifyDjangoPbkdf2(password: string, encoded: string) {
|
||
const [algorithm, iterationsText, salt, digestText] = encoded.split("$")
|
||
if (
|
||
algorithm !== "pbkdf2_sha256" ||
|
||
!iterationsText ||
|
||
!salt ||
|
||
!digestText
|
||
) {
|
||
return false
|
||
}
|
||
|
||
const iterations = Number(iterationsText)
|
||
const expected = Buffer.from(digestText, "base64")
|
||
if (
|
||
!Number.isSafeInteger(iterations) ||
|
||
iterations <= 0 ||
|
||
expected.length === 0
|
||
) {
|
||
return false
|
||
}
|
||
|
||
const actual = await pbkdf2Async(
|
||
password,
|
||
salt,
|
||
iterations,
|
||
expected.length,
|
||
"sha256",
|
||
)
|
||
return timingSafeEqual(actual, expected)
|
||
}
|
||
|
||
/**
|
||
* 验密码。**pbkdf2 那条分支永远不能删** —— 生产库 1710 个账号全是 Django 写的
|
||
* pbkdf2,只会在各自下次登录时才升级成 argon2,删掉就是全站登不上。
|
||
* 迭代次数是从哈希串里读的,所以 120000 到 1200000 的老哈希都验得了。
|
||
*/
|
||
export async function verifyPassword(password: string, encoded: string) {
|
||
if (encoded.startsWith("pbkdf2_sha256$")) {
|
||
return {
|
||
valid: await verifyDjangoPbkdf2(password, encoded),
|
||
needsUpgrade: true,
|
||
}
|
||
}
|
||
|
||
if (encoded.startsWith("$argon2")) {
|
||
return {
|
||
valid: await Bun.password.verify(password, encoded),
|
||
needsUpgrade: false,
|
||
}
|
||
}
|
||
|
||
return { valid: false, needsUpgrade: false }
|
||
}
|
||
|
||
/**
|
||
* 参数是显式写死的,不用 Bun 的默认值(`m=65536, t=2, p=1`,即每次哈希占 64 MiB)。
|
||
* 取的是 OWASP 对 argon2id 的推荐下限 `m=19MiB, t=2, p=1`:
|
||
*
|
||
* - 批量导入一个班要连算几十次哈希,64 MiB 那档单次 ~140ms,而 `oj-api` 的
|
||
* mem_limit 只有 512m(docker/compose.debian.yml),并发度被内存卡死。
|
||
* 19 MiB 这档单次 ~20ms,并发 4 路的峰值也才 76 MiB。
|
||
* - 参数是编码进哈希串本身的(`$argon2id$v=19$m=19456,t=2,p=1$...`),所以**存量
|
||
* 账号一个都不用迁移**,Bun.password.verify 读串里的参数验,改这里只影响此后新写的哈希。
|
||
*/
|
||
const ARGON2_OPTIONS = {
|
||
algorithm: "argon2id",
|
||
memoryCost: 19456,
|
||
timeCost: 2,
|
||
} as const
|
||
|
||
/**
|
||
* 写密码的**唯一入口**。五个调用方都走这里:注册、管理员改密码、批量导入用户、
|
||
* 重置密码、登录时升级存量 pbkdf2。
|
||
*
|
||
* 之所以特意收成一个函数:原来五处各写各的 `Bun.password.hash`,而当年那个
|
||
* 「回滚窗口内不要升级成 argon2」的开关只管住了登录
|
||
* 那一处,另外四处照写 argon2 不误 —— 老师给学生点一次「重置密码」,那个账号
|
||
* 就回不去旧站了,开关关着也拦不住。旧站 2026-08-26 下线,开关已经删掉,
|
||
* 但「只有一个地方写密码」这件事留下来了。
|
||
*
|
||
* 真要把旧站拉回来:切换手册「万一已经改坏了」那节的脚本才是正经退路 ——
|
||
* 它拿 `raw_password` 重算 Django 的 make_password,能修**已经**变成 argon2 的
|
||
* 账号;靠开关只能拦住将来,修不了已经发生的。
|
||
*/
|
||
export function hashPassword(password: string) {
|
||
return Bun.password.hash(password, ARGON2_OPTIONS)
|
||
}
|