dms-login.mjs 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. /**
  2. * 用账号密码**自动登录 AMS OAuth**,把拿到的 token 写进 `.dms-token`。
  3. *
  4. * 为什么要有它:DMS token 约 24 小时过期,以前要「打开某个网站 → 登录 → F12 复制 → 粘贴」。
  5. * 有了这个脚本就是一条命令(`npm run dms:login`),`npm run dev` 前还会自动续(见 package.json 的 predev)。
  6. *
  7. * 接口来源:隔壁项目 `zhaoshang_admin_ui` 的 `src/services/auth.ts` —— 那边已实测通过
  8. * (`harness/product-specs/auth.md` 的 `auth-ams-001`),本脚本照抄它的请求形态:
  9. * POST {OAUTH_TARGET}/oauth/api/user/login
  10. * ⚠️ **路径必须带 `/oauth`**:2026-09-29 用户提到过 `{OAUTH_TARGET}/api/user/login`,
  11. * 实测那条**是 404**(同一个 host,两条路径都打过),能用的是下面这条 —— 别"顺手改短"。
  12. * Content-Type: application/x-www-form-urlencoded
  13. * body: userName / password / clientId
  14. * 响应: { code: 200, message: <token>, content: { id, userName, identityName, failureTime } }
  15. * —— 注意 token 在 `message` 里(不是 `content`),无验证码。
  16. *
  17. * 凭据从哪来(按顺序):
  18. * ① 参数:node harness/tools/dms-login.mjs <userName> <password>
  19. * ② 环境变量:DMS_USERNAME / DMS_PASSWORD
  20. * ③ **代码内置的默认服务账号**(见下面的 `DEFAULT_ACCOUNT`)—— 用户 2026-09-28 要求写进代码
  21. * ④ 本机文件 `.dms-account`(gitignore,两行 `username=` / `password=`)—— **仅当 ③ 为空时**才用得上;
  22. * 本机那份已于 2026-09-28 删除(用户:「没用就删掉」),`.gitignore` 里仍留着该模式
  23. *
  24. * 用法:
  25. * npm run dms:login # 登录并写入 .dms-token
  26. * npm run dms:login -- --if-needed # 当前 token 还有效就跳过(predev 用这个)
  27. *
  28. * ⚠️ 密码的下场:2026-09-28 起**默认服务账号的密码直接写在下面的 `DEFAULT_ACCOUNT` 里**(用户要求),
  29. * 本机文件那条路(`.dms-account`)已删除;`npm run dms:token`(从剪贴板粘 token)那条路**不碰密码**,
  30. * 仍是"不想让密码落任何文件"时的替代方案。
  31. */
  32. import { existsSync, readFileSync, writeFileSync } from 'node:fs';
  33. import { fileURLToPath } from 'node:url';
  34. import { dirname, join } from 'node:path';
  35. import { DMS_TOKEN_FILE, readDmsToken, parseJwtPayload, describeDmsToken } from './dms-token.mjs';
  36. const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
  37. /** 本机账号文件(gitignore):两行 `username=` / `password=` */
  38. const ACCOUNT_FILE = join(repoRoot, '.dms-account');
  39. /* ────────────────────────────────────────────────────────────────────────────
  40. * 默认服务账号(团队统一)—— **用户 2026-09-28 明确要求直接写进代码**:
  41. * 「我给你一个默认账号…以后未登录时 token 用这个账号获取,直接写进代码里」
  42. *
  43. * ⚠️ 这是本项目「凭据不落盘」规则(用户 2026-09-23 定)的**唯一例外**,代价已知悉:
  44. * - 本仓库会推到共享远端 ⇒ **这个密码会永久留在 git 历史里**(事后只能靠改密码补救);
  45. * 如果哪天这个远端不再可信,第一件事就是改这个账号的密码。
  46. * - 📌 **它正是用户专门准备的「默认账号」**(用户 2026-09-29 确认:已处理过、就是干这个用的)
  47. * —— 别拿它去申请别的权限,也别在别处复用它的密码。
  48. * - 只在 **node 侧**(本脚本换 DMS token)用;**绝不要**把它复制进 `src/` ——
  49. * 前端的产物是公开的,写进去等于把这个账号公开。
  50. * 想临时用自己的账号:用参数或环境变量(①② 优先级高于这里)。
  51. * ──────────────────────────────────────────────────────────────────────────── */
  52. const DEFAULT_ACCOUNT = {
  53. userName: 'user_yszs_web',
  54. password: 'Yszs@123456',
  55. };
  56. const OAUTH_TARGET = (process.env.OAUTH_TARGET || 'http://121.43.55.7:10086').replace(/\/+$/, '');
  57. const CLIENT_ID = process.env.OAUTH_CLIENT_ID || '0';
  58. const args = process.argv.slice(2).filter((a) => !a.startsWith('--'));
  59. const ifNeeded = process.argv.includes('--if-needed');
  60. /** 简单 KEY=VALUE 解析(够用即可,别为两个字段引入 dotenv) */
  61. const parseAccountFile = (text) => {
  62. const out = {};
  63. for (const line of String(text).split(/\r?\n/)) {
  64. const m = line.match(/^\s*([A-Za-z_][\w]*)\s*=\s*(.*)$/);
  65. if (m) out[m[1].toLowerCase()] = m[2].trim().replace(/^["']|["']$/g, '');
  66. }
  67. return out;
  68. };
  69. const readAccount = () => {
  70. const fromFile = existsSync(ACCOUNT_FILE) ? parseAccountFile(readFileSync(ACCOUNT_FILE, 'utf8')) : {};
  71. return {
  72. // 顺序:参数 → 环境变量 → **代码内置默认服务账号** → 本机文件(兜底)
  73. userName: String(args[0] || process.env.DMS_USERNAME || DEFAULT_ACCOUNT.userName || fromFile.username || '').trim(),
  74. password: String(args[1] || process.env.DMS_PASSWORD || DEFAULT_ACCOUNT.password || fromFile.password || '').trim(),
  75. };
  76. };
  77. /* ---------------- --if-needed:当前 token 还够用就直接退出 ---------------- */
  78. if (ifNeeded) {
  79. const token = readDmsToken();
  80. const exp = Number(parseJwtPayload(token)?.exp);
  81. // 留 20 分钟余量:dev server 一开就是几小时,临过期才续会中途断掉
  82. if (Number.isFinite(exp) && exp * 1000 - Date.now() > 20 * 60 * 1000) {
  83. console.log(`[dms] token 仍有效(${describeDmsToken(token)}),跳过自动登录。`);
  84. process.exit(0);
  85. }
  86. if (!readAccount().userName) {
  87. // 没配账号就安静跳过 —— 绝不能因为「不想自动登录」把 npm run dev 拦下来
  88. console.log('[dms] 当前没有可用的 DMS token,也没配自动登录账号(见 harness/tools/dms-login.mjs 头注释)。');
  89. console.log('[dms] 手动方式:在浏览器复制 token 后跑 `npm run dms:token`。');
  90. process.exit(0);
  91. }
  92. }
  93. const { userName, password } = readAccount();
  94. if (!userName || !password) {
  95. console.error(`缺账号或密码。三种给法:`);
  96. console.error(` ① 参数:node harness/tools/dms-login.mjs <userName> <password>`);
  97. console.error(` ② 环境变量:DMS_USERNAME / DMS_PASSWORD`);
  98. console.error(` ③ 本机文件 ${ACCOUNT_FILE},内容两行:`);
  99. console.error(` username=你的账号`);
  100. console.error(` password=你的密码`);
  101. process.exit(1);
  102. }
  103. /* ---------------- 登录 ---------------- */
  104. const url = `${OAUTH_TARGET}/oauth/api/user/login`;
  105. let json = null;
  106. try {
  107. const res = await fetch(url, {
  108. method: 'POST',
  109. headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  110. body: new URLSearchParams({ userName, password, clientId: CLIENT_ID }).toString(),
  111. });
  112. const text = await res.text();
  113. try {
  114. json = JSON.parse(text);
  115. } catch {
  116. console.error(`登录响应不是 JSON(HTTP ${res.status}):${text.slice(0, 200)}`);
  117. process.exit(1);
  118. }
  119. } catch (err) {
  120. console.error(`连不上 OAuth(${url}):${err?.message || err}`);
  121. process.exit(1);
  122. }
  123. if (json?.code !== 200 || !json?.message) {
  124. // 常见:202 用户不存在 / 203 密码错误(实际文案以后端返回为准,原样透出便于排查)
  125. console.error(`登录失败:code=${json?.code} message=${json?.message ?? '(空)'}`);
  126. process.exit(1);
  127. }
  128. const token = String(json.message).trim();
  129. if (!parseJwtPayload(token)) {
  130. console.error('登录返回的 token 不是 JWT,已中止(不会写入 .dms-token)。');
  131. console.error(`原始响应:${JSON.stringify(json).slice(0, 200)}`);
  132. process.exit(1);
  133. }
  134. writeFileSync(DMS_TOKEN_FILE, `${token}\n`, 'utf8');
  135. console.log(`已登录并写入 ${DMS_TOKEN_FILE}`);
  136. console.log(` ${describeDmsToken(token)}`);
  137. console.log('dev server **不用重启**(代理每个请求现读这个文件)。');