Files
Stev_Wang 4d9eb980af
ci / 质量门禁 (push) Canceled after 0s
feat(identity): 完成 A1 身份认证与用户管理全栈落地
背景:
- 开发计划 A1 批次要求登录/刷新/登出、用户生命周期、路由与按钮权限、
  权限三态用例;视觉已选定方案 B(主从分栏 + 分屏登录)。

改动:
- packages/shared:新增 identity 契约(User/Me/Login/Refresh/Department/Role)
  与 user:view 权限点;写接口 strict 校验。
- apps/api:新增 identity 模块与 Prisma expand 迁移(departments/users/roles/
  user_roles/role_permissions/refresh_tokens);Argon2id、JWT access+refresh
  旋转与 jti 吊销、登录失败锁定、首登强改、守卫链、/me、@Audit 上下文、幂等种子。
- apps/web:按方案 B 落地 /users、/users/new、/users/:id、分屏登录与 403;
  usePermission/<Can>、路由守卫、401 单飞刷新、停用会话兜底、Semi + token。
- e2e:新增 A1-QA-01 六类端到端用例(A1_E2E 门控)。
- openapi 与前端生成类型同步;.env.example 登记认证变量;实施报告入库。

影响面:
- 新增运行时依赖 @nestjs/jwt、argon2(计划 A1-BE-02 已批准,版本精确锁定)。
- 迁移为 expand,不改既有表;本地需 prisma migrate 后重新 seed。
- 平台导航「用户与组织」由 /platform/users 调整为 /users。

回滚方式:
- 代码整体回滚本提交;迁移为 expand,回滚代码不需删表;
- 本地如需彻底回退按实施报告 §6 处理。

验证证据:
- pnpm format:check / lint / typecheck 通过;
- pnpm test:shared 95 / api 60 / web 55;
- pnpm test:int:7 个(真实 PG/Redis);
- A1_E2E=1 playwright test e2e/a1-identity.spec.ts:6/6;
- 报告:docs/开发计划/灵犀_A1身份认证与用户管理_实施报告_V0.1.md
2026-11-03 00:03:33 +08:00

71 lines
2.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* 端到端测试启动器:先起 vite preview,再跑 Playwright,结束后确保关闭预览服务。
* 不使用 Playwright webServer,避免 vite preview 在部分环境下退出挂起。
* 用法:pnpm test:e2e(会先执行 build)
*
* A1 账号生命周期用例(e2e/a1-identity.spec.ts):
* - 默认 A1_E2E=1 才执行,否则整文件 skip;本脚本默认不设该变量,因此 CI / pnpm test:e2e 不受影响;
* - 它需要**真实 API + web dev**(非 preview):
* 1) API 跑在 http://127.0.0.1:3000/api/v1(真实 PG / Redis);
* 2) web 跑在 http://127.0.0.1:5173(`pnpm --filter @lingxi/web dev`,需关闭 demo 会话);
* 3) 执行:A1_E2E=1 pnpm exec playwright test e2e/a1-identity.spec.ts --reporter=list
* - 默认 baseURL 走 A1_E2E_BASE_URL(缺省 5173),API 走 A1_E2E_API_BASE_URL(缺省 3000);
* 若改到 4173(preview)执行,需要 API 的 WEB_ORIGIN 同时包含 4173,否则浏览器请求被 CORS 拦截;
* - 截图落盘到 design/a1-ui-v1/shots/impl/e2e/。
*/
import { spawn, spawnSync } from 'node:child_process';
import { setTimeout as delay } from 'node:timers/promises';
const baseUrl = 'http://127.0.0.1:4173';
const pnpm = process.env.npm_execpath ? [process.execPath, process.env.npm_execpath] : ['pnpm'];
function runPnpm(args, options = {}) {
return spawnSync(pnpm[0], [...pnpm.slice(1), ...args], {
env: process.env,
stdio: 'inherit',
...options,
});
}
const server = spawn(pnpm[0], [...pnpm.slice(1), '--filter', '@lingxi/web', 'preview'], {
env: process.env,
stdio: 'ignore',
detached: true,
});
async function waitForServer(url, timeoutMs = 60_000) {
const startedAt = Date.now();
while (Date.now() - startedAt < timeoutMs) {
try {
const response = await fetch(url);
if (response.ok) return;
} catch {
// 预览服务尚未就绪,继续重试。
}
await delay(500);
}
throw new Error(`预览服务在 ${timeoutMs}ms 内未就绪:${url}`);
}
let exitCode = 1;
try {
await waitForServer(`${baseUrl}/login`);
const result = runPnpm(['exec', 'playwright', 'test']);
exitCode = result.status ?? 1;
} finally {
try {
process.kill(-server.pid, 'SIGTERM');
} catch {
// 进程可能已退出。
}
await delay(1_000);
try {
process.kill(-server.pid, 'SIGKILL');
} catch {
// 进程可能已退出。
}
}
process.exit(exitCode);