@xinhehuimeng/cli 0.2.4 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/package.json +1 -1
- package/src/commands/member.js +39 -1
- package/src/commands/session.js +59 -6
- package/src/spec/command_surface.json +69 -0
- package/src/version.js +1 -1
package/README.md
CHANGED
|
@@ -13,6 +13,25 @@ npx @xinhehuimeng/cli login --phone 13800000000 --code 123456
|
|
|
13
13
|
npx @xinhehuimeng/cli me --json
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
管理员为普通成员发放 30 分钟临时口令后,可以改用自己的长期口令:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# 1) 用临时口令登录
|
|
20
|
+
printf '%s' "$TEMP_PW" | xinhe-oa-cli login --password --account <手机号>
|
|
21
|
+
|
|
22
|
+
# 2) 首次登录必须改密;改密会吊销该账号全部旧会话
|
|
23
|
+
printf '%s\n%s\n%s\n' "$TEMP_PW" "$NEW_PW" "$NEW_PW" | \
|
|
24
|
+
xinhe-oa-cli login --change-password
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
管理员发放临时口令:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
xinhe-oa-cli member --reset-password <USER_ID> --yes --json
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
临时口令只显示一次,不要发到工单评论或群聊。
|
|
34
|
+
|
|
16
35
|
装一次就够(`npm i -g @xinhehuimeng/cli` 之后直接用 `xinhe-oa-cli`):
|
|
17
36
|
|
|
18
37
|
```bash
|
package/package.json
CHANGED
package/src/commands/member.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// 成员域:`member --list|--search|--create|--role|--revoke-role|--remove
|
|
1
|
+
// 成员域:`member --list|--search|--create|--role|--revoke-role|--remove|--reset-password`,
|
|
2
2
|
// 含 ORG-001 的部门归属三件套(`--set-dept` / `--unset-dept` / `--set-manager|--clear-manager`)、
|
|
3
3
|
// ORG-002 的授权三动作(`--grant|--revoke-grant|--grants`,issue #82)、
|
|
4
4
|
// ORG-004 的离职 / 移交四动作(issue #81)与 MEMBER-001 的 `--search`(issue #99)。
|
|
@@ -81,6 +81,8 @@ export async function handleMember({ args, runtime }) {
|
|
|
81
81
|
return revokeRole(runtime, args, action.role, action.user);
|
|
82
82
|
case 'remove':
|
|
83
83
|
return removeMember(runtime, action.user, args);
|
|
84
|
+
case 'reset-password':
|
|
85
|
+
return resetPassword(runtime, action.user);
|
|
84
86
|
case 'set-dept':
|
|
85
87
|
return setDept(runtime, args, action.user, action.deptId, action.primary);
|
|
86
88
|
case 'unset-dept':
|
|
@@ -129,6 +131,7 @@ function memberAction(args) {
|
|
|
129
131
|
['--role', given('--role') && !on('--create')],
|
|
130
132
|
['--revoke-role', given('--revoke-role')],
|
|
131
133
|
['--remove', given('--remove')],
|
|
134
|
+
['--reset-password', given('--reset-password')],
|
|
132
135
|
['--set-dept', given('--set-dept')],
|
|
133
136
|
['--unset-dept', given('--unset-dept')],
|
|
134
137
|
['--set-manager', given('--set-manager')],
|
|
@@ -396,6 +399,15 @@ function memberAction(args) {
|
|
|
396
399
|
}
|
|
397
400
|
return { kind: 'transfer-revert', transferId, note: String(note) };
|
|
398
401
|
}
|
|
402
|
+
case '--reset-password':
|
|
403
|
+
return {
|
|
404
|
+
kind: 'reset-password',
|
|
405
|
+
user: requiredId(
|
|
406
|
+
val('--reset-password'),
|
|
407
|
+
'--reset-password',
|
|
408
|
+
'例:xinhe-oa-cli member --reset-password usr_123 --yes --json',
|
|
409
|
+
),
|
|
410
|
+
};
|
|
399
411
|
default:
|
|
400
412
|
return { kind: 'remove', user: String(val('--remove') ?? '') };
|
|
401
413
|
}
|
|
@@ -779,6 +791,32 @@ async function removeMember(runtime, userId, args) {
|
|
|
779
791
|
});
|
|
780
792
|
}
|
|
781
793
|
|
|
794
|
+
/** 管理员发放 30 分钟一次性口令,并吊销该成员全部旧会话。 */
|
|
795
|
+
async function resetPassword(runtime, userId) {
|
|
796
|
+
runtime.requireYes(
|
|
797
|
+
'重置成员口令',
|
|
798
|
+
`xinhe-oa-cli member --reset-password ${userId}`,
|
|
799
|
+
);
|
|
800
|
+
const path = `/admin/users/${userId}/password/reset`;
|
|
801
|
+
const spec = requestSpec();
|
|
802
|
+
if (runtime.dryRun) return runtime.dryRunReport('POST', path, spec);
|
|
803
|
+
const response = await runtime.sendAuthed('POST', path, spec);
|
|
804
|
+
const data = dataOf(response);
|
|
805
|
+
runtime.finishFrom(response, {
|
|
806
|
+
user: data.user ?? null,
|
|
807
|
+
temporary_password: data.temporary_password ?? null,
|
|
808
|
+
expires_at: data.expires_at ?? null,
|
|
809
|
+
sessions_revoked: data.sessions_revoked ?? null,
|
|
810
|
+
must_change_password: data.must_change_password ?? null,
|
|
811
|
+
next_steps: [
|
|
812
|
+
'通过安全渠道把临时口令单独交给本人,不要发群聊或工单评论',
|
|
813
|
+
"本人登录:printf '%s' \"$TEMP_PW\" | xinhe-oa-cli login --password --account <手机号>",
|
|
814
|
+
"本人立即改密:printf '%s\\n%s\\n%s\\n' \"$TEMP_PW\" \"$NEW_PW\" \"$NEW_PW\" | xinhe-oa-cli login --change-password",
|
|
815
|
+
],
|
|
816
|
+
warning: '旧口令与该成员的全部旧会话已失效;临时口令只显示这一次。',
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
|
|
782
820
|
// ---------------------------------------------------------------------------
|
|
783
821
|
// 部门归属(ORG-001):set-dept / unset-dept / set-manager
|
|
784
822
|
// ---------------------------------------------------------------------------
|
package/src/commands/session.js
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
import { flagOn, flagValue } from '../core/args.js';
|
|
34
34
|
import {
|
|
35
35
|
adoptSession,
|
|
36
|
+
credentialFromTokens,
|
|
36
37
|
envelopeData,
|
|
37
38
|
loginWithPassword,
|
|
38
39
|
logout as logoutSession,
|
|
@@ -53,8 +54,9 @@ const SCOPE_CAP = 50;
|
|
|
53
54
|
* 顺序不是随手写的:clap 的 `ArgGroup(multiple = false)` 保证四选一,
|
|
54
55
|
* 但"既有 code 又 adopt"这种组合在 Rust 里由 adopt 先赢 —— `meta.command` 要一致。
|
|
55
56
|
*/
|
|
56
|
-
function loginKind({ adopt, password, hasCode }) {
|
|
57
|
+
function loginKind({ adopt, password, changePassword, hasCode }) {
|
|
57
58
|
if (adopt) return '--adopt-session';
|
|
59
|
+
if (changePassword) return '--change-password';
|
|
58
60
|
if (password) return '--password';
|
|
59
61
|
if (hasCode) return '--phone --code';
|
|
60
62
|
return '--phone';
|
|
@@ -64,6 +66,7 @@ function loginKind({ adopt, password, hasCode }) {
|
|
|
64
66
|
export async function handleLogin({ args, runtime }) {
|
|
65
67
|
const adopt = flagOn(args, '--adopt-session');
|
|
66
68
|
const password = flagOn(args, '--password');
|
|
69
|
+
const changePassword = flagOn(args, '--change-password');
|
|
67
70
|
const phone = flagValue(args, '--phone');
|
|
68
71
|
const code = flagValue(args, '--code');
|
|
69
72
|
const account = flagValue(args, '--account');
|
|
@@ -72,15 +75,18 @@ export async function handleLogin({ args, runtime }) {
|
|
|
72
75
|
// (`login --phone` / `login --phone --code` / `login --password` / `login --adopt-session`),
|
|
73
76
|
// 而 Node 的命令表按域名折叠成 `login`。这里在收尾(以及发请求)之前把它校正成
|
|
74
77
|
// Rust 的形态,`--json` 的 `meta.command` 才逐字一致。
|
|
75
|
-
runtime.command = `login ${loginKind({ adopt, password, hasCode: code !== null })}`;
|
|
78
|
+
runtime.command = `login ${loginKind({ adopt, password, changePassword, hasCode: code !== null })}`;
|
|
76
79
|
|
|
77
80
|
// ① 交接会话(install.sh 用):不联网,只把它写进自己的凭据文件。
|
|
78
81
|
if (adopt) return adoptFromStdin(runtime);
|
|
79
82
|
|
|
80
|
-
// ②
|
|
83
|
+
// ② 当前会话改密:先完成临时口令登录,再读取三行口令。
|
|
84
|
+
if (changePassword) return changeCurrentPassword(runtime);
|
|
85
|
+
|
|
86
|
+
// ③ 口令登录(普通成员或引导账号;口令从 stdin 读,**不进 argv**)。
|
|
81
87
|
if (password) return loginByPassword(runtime, account);
|
|
82
88
|
|
|
83
|
-
//
|
|
89
|
+
// ④ 拿验证码换凭据。
|
|
84
90
|
//
|
|
85
91
|
// 判空用 `=== null`(而不是 falsy):`--phone=` 这种显式空串在 Rust 里是
|
|
86
92
|
// `Some("")`,会被原样发给服务端,由服务端回"号码不合法" —— CLI 不替它判。
|
|
@@ -91,12 +97,59 @@ export async function handleLogin({ args, runtime }) {
|
|
|
91
97
|
}
|
|
92
98
|
if (code !== null) return loginByCode(runtime, phone, code);
|
|
93
99
|
|
|
94
|
-
//
|
|
100
|
+
// ⑤ 只给手机号 = 发码。
|
|
95
101
|
return requestOtp(runtime, phone);
|
|
96
102
|
}
|
|
97
103
|
|
|
104
|
+
async function changeCurrentPassword(runtime) {
|
|
105
|
+
const raw = await readStdinText();
|
|
106
|
+
const lines = raw.split(/\r?\n/).map((line) => line.trim());
|
|
107
|
+
if (lines.length < 3 || lines.slice(0, 3).some((line) => line.length === 0)) {
|
|
108
|
+
throw CliError.usage('标准输入里需要当前口令、新口令、确认新口令三行').withHint(
|
|
109
|
+
"用法:printf '%s\\n%s\\n%s\\n' \"$OLD_PW\" \"$NEW_PW\" \"$NEW_PW\" | xinhe-oa-cli login --change-password",
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const [currentPassword, newPassword, confirmation] = lines;
|
|
113
|
+
if (newPassword !== confirmation) throw CliError.usage('两次输入的新口令不一致');
|
|
114
|
+
if (newPassword.length < 12) throw CliError.usage('新口令至少需要 12 个字符');
|
|
115
|
+
if (newPassword === currentPassword) throw CliError.usage('新口令不能与当前口令相同');
|
|
116
|
+
|
|
117
|
+
const spec = requestSpec({
|
|
118
|
+
body: { current_password: currentPassword, new_password: newPassword },
|
|
119
|
+
});
|
|
120
|
+
if (runtime.dryRun) {
|
|
121
|
+
return runtime.dryRunReport('POST', '/auth/password/change', spec);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const response = await runtime.sendAuthed('POST', '/auth/password/change', spec);
|
|
125
|
+
const body = envelopeData(response);
|
|
126
|
+
const previous = runtime.credentials.get(runtime.profile);
|
|
127
|
+
const phone = previous?.phone ?? runtime.config.profile(runtime.profile).phone ?? null;
|
|
128
|
+
const deviceId = runtime.ensureDeviceId();
|
|
129
|
+
const credential = credentialFromTokens(body, runtime.baseUrl, deviceId, phone);
|
|
130
|
+
runtime.credentials.put(runtime.profile, credential);
|
|
131
|
+
if (typeof phone === 'string' && phone.length > 0) {
|
|
132
|
+
runtime.config.setPhone(phone);
|
|
133
|
+
runtime.config.save();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return runtime.finishFrom(response, {
|
|
137
|
+
profile: runtime.profile,
|
|
138
|
+
base_url: credential.base_url,
|
|
139
|
+
user: credential.user ?? null,
|
|
140
|
+
auth_method: 'password',
|
|
141
|
+
sessions_revoked: body.sessions_revoked ?? null,
|
|
142
|
+
credentials_file: runtime.paths.credentialsFile,
|
|
143
|
+
access_token: maskToken(credential.tokens?.access_token ?? ''),
|
|
144
|
+
refresh_token: maskToken(credential.tokens?.refresh_token ?? ''),
|
|
145
|
+
access_expires_at: credential.tokens?.access_expires_at ?? null,
|
|
146
|
+
next_step: '口令已更新并保存新会话;现在可以执行 `xinhe-oa-cli me --json` 或每日任务',
|
|
147
|
+
note: '旧口令、全部旧会话和临时口令均已失效;新令牌未进入 argv 与日志。',
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
98
151
|
/**
|
|
99
|
-
* `login --password --account
|
|
152
|
+
* `login --password --account`:普通成员或引导账号,口令从 stdin 读。
|
|
100
153
|
*
|
|
101
154
|
* 三步的顺序是刻意的(与 Rust 一致):**先落盘 device_id,再读口令,最后才碰网络**。
|
|
102
155
|
* 口令读失败(stdin 是空的)时,device_id 已经存好了 —— 用户重跑一次不会换指纹。
|
|
@@ -299,6 +299,43 @@
|
|
|
299
299
|
"--password"
|
|
300
300
|
]
|
|
301
301
|
},
|
|
302
|
+
{
|
|
303
|
+
"name": "login --change-password",
|
|
304
|
+
"flags": {
|
|
305
|
+
"--change-password": {
|
|
306
|
+
"takes_value": false,
|
|
307
|
+
"required": false,
|
|
308
|
+
"optional_value": false
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
"extra_flags": [],
|
|
312
|
+
"allowed_flags": [],
|
|
313
|
+
"params_raw": [
|
|
314
|
+
"--change-password"
|
|
315
|
+
],
|
|
316
|
+
"params": [
|
|
317
|
+
{
|
|
318
|
+
"flag": "--change-password"
|
|
319
|
+
}
|
|
320
|
+
],
|
|
321
|
+
"risk": "write",
|
|
322
|
+
"requires_yes": false,
|
|
323
|
+
"endpoints": [
|
|
324
|
+
"POST /auth/password/change"
|
|
325
|
+
],
|
|
326
|
+
"examples": [
|
|
327
|
+
"printf '%s\\n%s\\n%s\\n' \"$OLD_PW\" \"$NEW_PW\" \"$NEW_PW\" | xinhe-oa-cli login --change-password"
|
|
328
|
+
],
|
|
329
|
+
"output_schema": "{user, auth_method, sessions_revoked, tokens_saved: true}(旧会话与临时口令失效)",
|
|
330
|
+
"synonym_flags": [],
|
|
331
|
+
"rejected_flags": [
|
|
332
|
+
"--phone",
|
|
333
|
+
"--code",
|
|
334
|
+
"--password",
|
|
335
|
+
"--account",
|
|
336
|
+
"--adopt-session"
|
|
337
|
+
]
|
|
338
|
+
},
|
|
302
339
|
{
|
|
303
340
|
"name": "logout",
|
|
304
341
|
"flags": {},
|
|
@@ -26809,6 +26846,38 @@
|
|
|
26809
26846
|
"--primary"
|
|
26810
26847
|
]
|
|
26811
26848
|
},
|
|
26849
|
+
{
|
|
26850
|
+
"name": "member --reset-password",
|
|
26851
|
+
"flags": {
|
|
26852
|
+
"--reset-password": {
|
|
26853
|
+
"takes_value": true,
|
|
26854
|
+
"required": false,
|
|
26855
|
+
"optional_value": false
|
|
26856
|
+
}
|
|
26857
|
+
},
|
|
26858
|
+
"extra_flags": [],
|
|
26859
|
+
"allowed_flags": [],
|
|
26860
|
+
"params_raw": [
|
|
26861
|
+
"--reset-password <USER_ID>"
|
|
26862
|
+
],
|
|
26863
|
+
"params": [
|
|
26864
|
+
{
|
|
26865
|
+
"flag": "--reset-password",
|
|
26866
|
+
"value": "USER_ID"
|
|
26867
|
+
}
|
|
26868
|
+
],
|
|
26869
|
+
"risk": "high-risk-write",
|
|
26870
|
+
"requires_yes": true,
|
|
26871
|
+
"endpoints": [
|
|
26872
|
+
"POST /admin/users/{id}/password/reset"
|
|
26873
|
+
],
|
|
26874
|
+
"examples": [
|
|
26875
|
+
"xinhe-oa-cli member --reset-password usr_123 --yes --json"
|
|
26876
|
+
],
|
|
26877
|
+
"output_schema": "{user, temporary_password, expires_at, sessions_revoked, must_change_password}(临时口令只显示一次,30 分钟有效)",
|
|
26878
|
+
"synonym_flags": [],
|
|
26879
|
+
"rejected_flags": []
|
|
26880
|
+
},
|
|
26812
26881
|
{
|
|
26813
26882
|
"name": "config --list",
|
|
26814
26883
|
"flags": {
|
package/src/version.js
CHANGED