原来只有 `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>
This commit is contained in:
@@ -29,23 +29,40 @@ export function contestStatus(contest: ContestRow) {
|
||||
return "0" as const
|
||||
}
|
||||
|
||||
export function isContestAdmin(user: AuthUser | null | undefined, contest: ContestRow) {
|
||||
return Boolean(user && (user.id === contest.createdById || user.adminType === "Super Admin"))
|
||||
export function isContestAdmin(
|
||||
user: AuthUser | null | undefined,
|
||||
contest: ContestRow,
|
||||
) {
|
||||
return Boolean(
|
||||
user &&
|
||||
(user.id === contest.createdById || user.adminType === "Super Admin"),
|
||||
)
|
||||
}
|
||||
|
||||
export function contestDetailsAllowed(user: AuthUser | null | undefined, contest: ContestRow) {
|
||||
export function contestDetailsAllowed(
|
||||
user: AuthUser | null | undefined,
|
||||
contest: ContestRow,
|
||||
) {
|
||||
return contestStatus(contest) === "-1" || isContestAdmin(user, contest)
|
||||
}
|
||||
|
||||
export function checkContestPassword(candidate: string | null | undefined, expected: string | null) {
|
||||
export function checkContestPassword(
|
||||
candidate: string | null | undefined,
|
||||
expected: string | null,
|
||||
) {
|
||||
if (!candidate || !expected) return false
|
||||
if (candidate === expected) return true
|
||||
const parts = candidate.split("#")
|
||||
if (parts.length !== 2) return false
|
||||
const [signature, expiresAt] = parts
|
||||
if (!signature || !expiresAt || !/^\d+$/.test(expiresAt)) return false
|
||||
const expectedSignature = createHash("sha256").update(`${expected}${expiresAt}`).digest("hex").slice(0, 8)
|
||||
return signature === expectedSignature && Date.now() < Number(expiresAt) * 1000
|
||||
const expectedSignature = createHash("sha256")
|
||||
.update(`${expected}${expiresAt}`)
|
||||
.digest("hex")
|
||||
.slice(0, 8)
|
||||
return (
|
||||
signature === expectedSignature && Date.now() < Number(expiresAt) * 1000
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,9 +75,15 @@ export function checkContestPassword(candidate: string | null | undefined, expec
|
||||
*
|
||||
* 放宽的只有出题人自己的视角,学生看隐藏比赛照旧是 404。
|
||||
*/
|
||||
export async function findAccessibleContest(user: AuthUser | null | undefined, id: number) {
|
||||
const [contest] = await db.select().from(schema.contest)
|
||||
.where(eq(schema.contest.id, id)).limit(1)
|
||||
export async function findAccessibleContest(
|
||||
user: AuthUser | null | undefined,
|
||||
id: number,
|
||||
) {
|
||||
const [contest] = await db
|
||||
.select()
|
||||
.from(schema.contest)
|
||||
.where(eq(schema.contest.id, id))
|
||||
.limit(1)
|
||||
if (!contest) return null
|
||||
return contest.visible || isContestAdmin(user, contest) ? contest : null
|
||||
}
|
||||
@@ -73,16 +96,25 @@ export async function canAccessContest<E extends AppEnv>(
|
||||
checkType: "details" | "problems" | "ranks" | "submissions",
|
||||
) {
|
||||
const user = c.get("user")
|
||||
if (!user) return { ok: false as const, code: "login-required", message: "请先登录" }
|
||||
if (!user)
|
||||
return { ok: false as const, code: "login-required", message: "请先登录" }
|
||||
if (isContestAdmin(user, contest)) return { ok: true as const }
|
||||
if (contest.password) {
|
||||
const stored = await getContestPassword(c, contest.id)
|
||||
if (!checkContestPassword(stored, contest.password)) {
|
||||
return { ok: false as const, code: "wrong-password", message: "Wrong password or password expired" }
|
||||
return {
|
||||
ok: false as const,
|
||||
code: "wrong-password",
|
||||
message: "Wrong password or password expired",
|
||||
}
|
||||
}
|
||||
}
|
||||
if (contestStatus(contest) === "1" && checkType !== "details") {
|
||||
return { ok: false as const, code: "contest-not-started", message: "Contest has not started yet." }
|
||||
return {
|
||||
ok: false as const,
|
||||
code: "contest-not-started",
|
||||
message: "Contest has not started yet.",
|
||||
}
|
||||
}
|
||||
return { ok: true as const }
|
||||
}
|
||||
@@ -104,11 +136,20 @@ export function requireContestAccess(
|
||||
): MiddlewareHandler<ContestEnv> {
|
||||
return async (c, next) => {
|
||||
const id = Number(c.req.param(paramName))
|
||||
const contest = Number.isInteger(id) && id > 0 ? await findAccessibleContest(c.get("user"), id) : null
|
||||
if (!contest) return failure(c, 404, "contest-not-found", "Contest does not exist")
|
||||
const contest =
|
||||
Number.isInteger(id) && id > 0
|
||||
? await findAccessibleContest(c.get("user"), id)
|
||||
: null
|
||||
if (!contest)
|
||||
return failure(c, 404, "contest-not-found", "Contest does not exist")
|
||||
const access = await canAccessContest(c, contest, checkType)
|
||||
if (!access.ok) {
|
||||
return failure(c, access.code === "login-required" ? 401 : 403, access.code, access.message)
|
||||
return failure(
|
||||
c,
|
||||
access.code === "login-required" ? 401 : 403,
|
||||
access.code,
|
||||
access.message,
|
||||
)
|
||||
}
|
||||
c.set("contest", contest)
|
||||
await next()
|
||||
|
||||
Reference in New Issue
Block a user