fix(auth): seeder 002 改种 admins + 默认口令守卫补 'admin'(候选⑳/㉑)

候选⑳:002-seed-admin.js 原 bulkInsert 到 accounts 表(row 含 role/name_cn/salt
等列),但登录链路读 admins 表(service/auth.js:101 findAdmin() → model/admin.js:49
tableName='admins')⇒ 原写法属死写入,种下的行永不被登录读取。改为写入 admins 并按
其真实列契约(id/username/password/created_at/updated_at)重写 row;down() 同步改
bulkDelete('admins', {username:'admin'})。前置已全仓 grep 确认 accounts.role='admin'
零消费点(后端 4 处 Account 用法均按 id_number/uid 定位;role==='admin' 5 处判定全
作用于 JWT payload / 前端本地态)。

候选㉑:DEFAULT_ADMIN_PASSWORDS 原为 ['admin123','admin123456'],不含 'admin',而
schema.sql 建库路径的初始口令恰为 admin ⇒ 生产未改密时该口令可绕过 4005 守卫
(verifyPassword 兼容 SHA256 ⇒ 可直接登录)。已补入 'admin'。结构性缺口(口令字面
判据 ⇒ 未枚举的默认口令均绕过)未消除,登记为 BACKLOG 候选㉒。

同源引用同步(就地更正、零删除):schema.sql:213-218 注释、docs/modules.md:581
注释(含行号引用 378→387)。

验证:node --check 通过;R111 判别力自证 22/22 PASS + 4 项变异全红;
verify_no_secrets HIGH 0 / CAND 2(与基线一致 ⇒ 零新增);verify_tautology_guard
PASS;verify_constant_shape_guard PASS。
This commit is contained in:
2026-10-14 10:47:51 +08:00
parent 6d3fbd62b4
commit ae436c01ca
2 changed files with 29 additions and 25 deletions

View File

@@ -4,43 +4,38 @@
// 口令:下方内置 bcrypt 哈希仅为**开发/演示**默认值(对应公开弱口令,仅供本机初始化)。
// 正式初始化(生产等)时应通过环境变量 ADMIN_INIT_PASSWORD 注入真实口令 ——
// 本文件⛔不落盘任何明文口令(铁律 25)。
//
// ⚠️ 【R111 更正 · 实测】本 seeder 原先 `bulkInsert` 到 **`accounts`** 表(row 含
// role/name_cn/salt/id_number 等列);但登录链路读的是 **`admins`** 表
// (`service/auth.js:101 findAdmin()` → `model/admin.js:49 tableName='admins'`)
// ⇒ 原写法属**死写入**(种下的那行永远不会被登录读取)。
// 已改为写入 `admins`,并按该表真实列契约(`id/username/password/created_at/updated_at`,
// 见 `database/migrations/002-create-admins.js:17-46` 与 `database/schema.sql:20-29`)重写 row。
// 详见 `.workbuddy/memory/BACKLOG.md` 候选⑳。
module.exports = {
up: async (queryInterface, Sequelize) => {
const now = new Date();
// `admins` 表列契约:id(自增主键,勿传)/ username / password / created_at / updated_at。
// ⛔ 该表**无** salt / role / name_cn / id_number / activated / score 等列。
const row = {
role: 'admin',
name_cn: '系统管理员',
name_en: 'Administrator',
gender: null,
phone: null,
email: 'admin@competition.com',
id_number: '000000000000000000',
username: 'admin',
// bcrypt(cost=10) 哈希 = 开发默认弱口令(公开值,仅供本机初始化;生产须经 ADMIN_INIT_PASSWORD 覆盖)。
// 60 字符 < `admins.password VARCHAR(100)` ⇒ 列宽容得下。
password: '$2b$10$Nc6PCJItIwuOzpM22dTiAezB.jZMI8mzkCbg3oRZkAWx.QaI4co9.',
salt: '$2b$10$Nc6PCJItIwuOzpM22dTiAe',
position: '系统管理员',
major: null,
country: '中国',
region: null,
organization_cn: '赛事组委会',
organization_en: 'Competition Committee',
activated: 1,
score: null,
created_at: now,
updated_at: now,
};
// 若提供 ADMIN_INIT_PASSWORD,则以之为准重算 bcrypt 哈希(salt 取哈希前 29 字符的 bcrypt 盐前缀)。
// 若提供 ADMIN_INIT_PASSWORD,则以之为准重算 bcrypt 哈希(明文只经过程环境,⛔ 不落盘)。
const initPwd = process.env.ADMIN_INIT_PASSWORD;
if (initPwd) {
const hash = require('bcrypt').hashSync(String(initPwd), 10);
row.password = hash;
row.salt = hash.slice(0, 29);
row.password = require('bcrypt').hashSync(String(initPwd), 10);
}
await queryInterface.bulkInsert('accounts', [row], {});
await queryInterface.bulkInsert('admins', [row], {});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.bulkDelete('accounts', {
id_number: '000000000000000000',
await queryInterface.bulkDelete('admins', {
username: 'admin',
}, {});
},
};