三条迁移,一次部署(0008/0009 含 DROP COLUMN,需要 OJ2_ALLOW_DESTRUCTIVE=1): - 0008 删 IP 相关:比赛 IP 白名单(前端本来就没有输入框,detail.vue 无条件置空)、 submission.ip(前端从未显示过)、以及一次都没被调用过的 IP 限流桶。 judge_server.ip 是运维数据,保留。 - 0009 删九个只有 Django 时代写过、OJ2 一次都没读过的列:user 的 auth_token / open_api / open_api_appkey / session_keys,user_profile 的 blog / github / school / major / language。open_api 后台连开关都没有,那段「已经开着就不重置 appkey」的逻辑从上线起没进过 if。判据是「全仓零读取」而不是「看着没用」—— raw_password 同样刺眼却是在用的,别一起清掉。 - 0010 给 17 条外键补上删除动作,不再是 Django 留下的一律 NO ACTION。父行消失后 必然无意义、且不构成学生留痕的走 CASCADE(中间表、题单/教程/成就的组成部分、 user_profile 与 user_stat);需要人看见的继续拦着——submission.problem_id、 以及 user 的绝大多数外键,删用户撞外键会被 handler 翻译成「请改为禁用账号」, 这是有意的:全 CASCADE 会静默抹掉成就与进度,而 submission.user_id 压根没有 外键,结果是一半删一半留。六处手工级联随之删掉。 角色字符串收进 packages/contract/src/roles.ts:原先 ADMIN_ROLES / TEACHER_ROLES 在两个文件各抄一份、学生口径在四个文件各写一遍、前端 USER_TYPE 是第三份副本。 AuthUser.adminType 与 drizzle 的列都收窄成联合类型,二十多处 `=== "Super Admin"` 从此受编译器管着($type 是纯 TS 层的,generate 确认不产生任何 SQL 变更)。 顺带删掉 db/relations.ts —— drizzle-kit pull 的产物,全仓零引用。 一处行为变化:后台用户列表传非法的 ?type= 回 400,不再静默返回空列表;界面上的 下拉只有合法值,打不到这条。 验证:tsc / vue-tsc / vite build / check:routes 全过;三条迁移在 dev 库执行, 并逐条建 fixture 走 HTTP 接口验过删除连坐与拦截(题单五张子表连坐、user_badge 二级连坐、删有提交的题目仍 409、删有表情的用户仍 409)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AeJoYc2t2d7cThVqMBYrBF
115 lines
3.8 KiB
TypeScript
115 lines
3.8 KiB
TypeScript
import { eq, sql } from "drizzle-orm"
|
||
|
||
import { hashPassword } from "../auth/password"
|
||
import { db, schema } from "../db"
|
||
|
||
/**
|
||
* 本机开发用的账号。**只能对本地库跑** —— 它会重置账号密码并把明文写进
|
||
* raw_password,其中一个还是超管。对着生产库跑一次就是把超管密码改掉,
|
||
* 所以这里按 DATABASE_URL 的主机名拦一道,需要绕过时显式设 OJ2_SEED_FORCE=true。
|
||
*/
|
||
const url = process.env.DATABASE_URL ?? "postgres://onlinejudge:onlinejudge@localhost:5433/onlinejudge"
|
||
const host = (() => {
|
||
try {
|
||
return new URL(url).hostname
|
||
} catch {
|
||
return ""
|
||
}
|
||
})()
|
||
const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "::1", ""])
|
||
if (!LOCAL_HOSTS.has(host) && process.env.OJ2_SEED_FORCE !== "true") {
|
||
console.error(
|
||
`[seed:dev] DATABASE_URL 指向 ${host},不是本机。这个脚本会重置账号密码` +
|
||
`(含一个超管)并写明文 raw_password,拒绝执行。\n` +
|
||
` 确实要对这个库跑,设 OJ2_SEED_FORCE=true。`,
|
||
)
|
||
process.exit(1)
|
||
}
|
||
|
||
const now = new Date().toISOString()
|
||
|
||
// 阶段 1 是带着显式 id 导入的,PostgreSQL 的序列没跟着走,先对齐
|
||
await db.execute(
|
||
sql`select setval(pg_get_serial_sequence('"user"', 'id'), coalesce(max(${schema.user.id}), 1), true) from ${schema.user}`,
|
||
)
|
||
|
||
interface SeedAccount {
|
||
username: string
|
||
password: string
|
||
adminType: "Regular User" | "Super Admin"
|
||
problemPermission: "None" | "All"
|
||
realName: string
|
||
}
|
||
|
||
/**
|
||
* 建号或就地更新。**user_profile 那一行不能省** —— 缺了它 `/api/me` 返回
|
||
* profile-not-found,前端的 getMyProfile 抛异常、localStorage 的 authed 存不
|
||
* 进去,于是所有 /admin 路由被守卫弹回首页,而且不报错。本机的 devadmin 原来
|
||
* 就缺 profile 和 email,后台页面一律进不去,排查了很久才找到这里。
|
||
*/
|
||
async function seed(account: SeedAccount) {
|
||
// 走 hashPassword,和线上五个写入点同一条路
|
||
const passwordHash = await hashPassword(account.password)
|
||
const email = `${account.username}@example.test`
|
||
const [user] = await db
|
||
.insert(schema.user)
|
||
.values({
|
||
username: account.username,
|
||
password: passwordHash,
|
||
rawPassword: account.password,
|
||
email,
|
||
createTime: now,
|
||
adminType: account.adminType,
|
||
problemPermission: account.problemPermission,
|
||
isDisabled: false,
|
||
})
|
||
.onConflictDoUpdate({
|
||
target: schema.user.username,
|
||
set: {
|
||
password: passwordHash,
|
||
rawPassword: account.password,
|
||
email,
|
||
adminType: account.adminType,
|
||
problemPermission: account.problemPermission,
|
||
isDisabled: false,
|
||
},
|
||
})
|
||
.returning({ id: schema.user.id, username: schema.user.username })
|
||
|
||
if (!user) throw new Error(`Failed to seed ${account.username}`)
|
||
|
||
const [profile] = await db
|
||
.select({ id: schema.userProfile.id })
|
||
.from(schema.userProfile)
|
||
.where(eq(schema.userProfile.userId, user.id))
|
||
.limit(1)
|
||
|
||
if (!profile) {
|
||
await db.insert(schema.userProfile).values({
|
||
userId: user.id,
|
||
acmProblemsStatus: { problems: {}, contest_problems: {} },
|
||
avatar: "/public/avatar/default.png",
|
||
realName: account.realName,
|
||
})
|
||
}
|
||
|
||
console.log(` ${account.adminType.padEnd(13)} ${user.username} / ${account.password}`)
|
||
}
|
||
|
||
console.log("Seeded development logins:")
|
||
await seed({
|
||
username: process.env.OJ2_DEV_USERNAME ?? "student",
|
||
password: process.env.OJ2_DEV_PASSWORD ?? "student123",
|
||
adminType: "Regular User",
|
||
problemPermission: "None",
|
||
realName: "开发用学生",
|
||
})
|
||
await seed({
|
||
username: process.env.OJ2_DEV_ADMIN_USERNAME ?? "devadmin",
|
||
password: process.env.OJ2_DEV_ADMIN_PASSWORD ?? "devadmin123",
|
||
adminType: "Super Admin",
|
||
problemPermission: "All",
|
||
realName: "开发用超管",
|
||
})
|
||
process.exit(0)
|