103 lines
3.6 KiB
TypeScript
103 lines
3.6 KiB
TypeScript
|
|
// services/api/src/scripts/seed-it-users.ts —— P3 Task 11 集成测试专用
|
|||
|
|
// 用户种子脚本(MuMu + 真实后端套件的预置步骤,由 scripts/run-mobile-it.sh
|
|||
|
|
// 调用)。运行方式:`IT_USERS="a:pw1,b:pw2" pnpm --filter @photo/api-cn seed:it`
|
|||
|
|
// (= tsc 构建后 node dist/scripts/seed-it-users.js)。
|
|||
|
|
//
|
|||
|
|
// 与共享 dev 库的隔离约定(集成测试【不做 DB reset】,见脚本头注释):
|
|||
|
|
// - 每次运行由编排脚本生成【全新唯一账号】(时间戳后缀)传入本脚本;
|
|||
|
|
// - 本脚本只做「不存在则建」,存在即跳过且【绝不改写密码/角色】——
|
|||
|
|
// 与 seed-admin 同幂等语义,重复调用安全;
|
|||
|
|
// - 角色固定 staff:相册创建/邀请查找在 cn 侧仅要求已认证(SessionGuard),
|
|||
|
|
// 测试账号无需 admin 角色。
|
|||
|
|
import { existsSync } from 'node:fs';
|
|||
|
|
import { dirname, join } from 'node:path';
|
|||
|
|
import { config } from 'dotenv';
|
|||
|
|
import { PrismaClient } from '@photo/db-cn';
|
|||
|
|
import { hashPassword } from '../auth/password';
|
|||
|
|
|
|||
|
|
/** 解析 `account:password,...` 列表(冒号冲突风险可接受:账号不含冒号)。 */
|
|||
|
|
export function parseItUsers(raw: string): Array<{ account: string; password: string }> {
|
|||
|
|
return raw
|
|||
|
|
.split(',')
|
|||
|
|
.map((pair) => pair.trim())
|
|||
|
|
.filter((pair) => pair !== '')
|
|||
|
|
.map((pair) => {
|
|||
|
|
const sep = pair.indexOf(':');
|
|||
|
|
if (sep <= 0) {
|
|||
|
|
throw new Error(`seed-it-users: 非法条目 "${pair}"(应为 account:password)`);
|
|||
|
|
}
|
|||
|
|
const account = pair.slice(0, sep).trim();
|
|||
|
|
const password = pair.slice(sep + 1);
|
|||
|
|
if (account === '' || password === '') {
|
|||
|
|
throw new Error(`seed-it-users: 空账号/密码(${pair})`);
|
|||
|
|
}
|
|||
|
|
return { account, password };
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 种子核心(可测):逐个建号,已存在跳过。返回账号 → 是否新建。 */
|
|||
|
|
export async function seedItUsers(
|
|||
|
|
prisma: PrismaClient,
|
|||
|
|
users: Array<{ account: string; password: string }>,
|
|||
|
|
): Promise<Map<string, boolean>> {
|
|||
|
|
const result = new Map<string, boolean>();
|
|||
|
|
for (const { account, password } of users) {
|
|||
|
|
const existing = await prisma.users.findUnique({ where: { account } });
|
|||
|
|
if (existing !== null) {
|
|||
|
|
result.set(account, false);
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
await prisma.users.create({
|
|||
|
|
data: {
|
|||
|
|
account,
|
|||
|
|
passwordHash: await hashPassword(password),
|
|||
|
|
displayName: account,
|
|||
|
|
role: 'staff',
|
|||
|
|
credentialStatus: 'active',
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
result.set(account, true);
|
|||
|
|
}
|
|||
|
|
return result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 定位仓库根 .env(与 seed-admin 相同约定:不覆盖已有进程环境变量)。 */
|
|||
|
|
function loadRootEnv(): void {
|
|||
|
|
let dir = __dirname;
|
|||
|
|
while (!existsSync(join(dir, 'pnpm-workspace.yaml'))) {
|
|||
|
|
const parent = dirname(dir);
|
|||
|
|
if (parent === dir) {
|
|||
|
|
throw new Error('seed-it-users: pnpm-workspace.yaml not found');
|
|||
|
|
}
|
|||
|
|
dir = parent;
|
|||
|
|
}
|
|||
|
|
config({ path: join(dir, '.env') });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function main(): Promise<void> {
|
|||
|
|
loadRootEnv();
|
|||
|
|
const raw = process.env.IT_USERS;
|
|||
|
|
if (!raw) {
|
|||
|
|
throw new Error('seed-it-users: IT_USERS 环境变量缺失(格式 account:password,...)');
|
|||
|
|
}
|
|||
|
|
const users = parseItUsers(raw);
|
|||
|
|
const prisma = new PrismaClient();
|
|||
|
|
try {
|
|||
|
|
const created = await seedItUsers(prisma, users);
|
|||
|
|
for (const [account, wasCreated] of created) {
|
|||
|
|
console.log(
|
|||
|
|
`seed-it-users: ${account} ${wasCreated ? 'created (staff)' : 'exists, skipped'}`,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
} finally {
|
|||
|
|
await prisma.$disconnect();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (require.main === module) {
|
|||
|
|
void main().catch((err: unknown) => {
|
|||
|
|
console.error(err instanceof Error ? err.message : err);
|
|||
|
|
process.exitCode = 1;
|
|||
|
|
});
|
|||
|
|
}
|