diff --git a/services/zhizhangyi__mbs/README.md b/services/zhizhangyi__mbs/README.md new file mode 100644 index 00000000..546ffd7d --- /dev/null +++ b/services/zhizhangyi__mbs/README.md @@ -0,0 +1,193 @@ +# Zhizhangyi MBS + +This service package exposes Zhizhangyi MBS (Mobile Security Management Platform) user management APIs through OctoBus. + +The package is scoped to MBS user operations: listing users, reading user details, checking login names, creating and updating users, deleting users, enabling or disabling users, updating passwords, forcing users offline, and importing users. + +## Configuration + +Configure the target MBS endpoint in the service config: + +```json +{ + "endpoint": "https://mbs.example.com:9074", + "skipTlsVerify": false, + "timeoutMs": 30000 +} +``` + +`endpoint` is the MBS REST API base URL. Use a hostname or sanitized placeholder in shared examples. Do not commit real internal IP addresses. + +Configure credentials in the service secret: + +```json +{ + "appkey": "", + "secretkey": "", + "orgCode": "" +} +``` + +Do not commit real `appkey`, `secretkey`, `orgCode`, password ciphertext, or generated `sign` values. + +## Authentication + +MBS signs requests as: + +```text +MD5(appkey + param1 + param2 + ... + secretkey) +``` + +The adapter computes signatures when `sign` is not provided. Password fields are forwarded as caller-provided MBS 3DES-encrypted strings; this package does not derive or perform password encryption. + +## Methods + +All methods are exposed under the `zhizhangyi.mbs.UserManagement` service. + +| Method | Purpose | Key request fields | +| --- | --- | --- | +| `GetUsers` | List users with pagination, sorting, and filters. | `index`, `size`, `orderCode`, `orderType`, `condition.deptId` required, optional `condition.keyWord`, `condition.state`, `condition.isMdm`. | +| `AddUser` | Create a user. | `userName`, `loginName`, `deptId`, `password` required; optional contact fields, `userSource`, `isMdm`, `state`, `weight`, `attrs`. | +| `UpdUser` | Update a user. | `userId`, `userName`, `deptId` required; optional `loginName`, contact fields, `isMdm`, `weight`, `attrs`. | +| `DetailUser` | Get user detail by ID. | `userId` required. | +| `DelUsers` | Delete users by ID list or by condition. | `type=0` with `userIds`, or `type=1` with `condition`. | +| `StateUsers` | Enable or disable users by ID list or condition. | `state` required; `type=0` with `userIds`, or `type=1` with `condition`. | +| `CheckLoginName` | Check login name availability. | `loginName` required. | +| `GetUserByPhone` | Find users by phone number. | `phone` required. | +| `UpdUserPwd` | Update a password using v1 admin mode or v2 self-service mode. | `version` must be `v1` or `v2`; v1 requires `userId` and `password`; v2 requires `loginName` and `newPwd`, optional `oldPwd`. | +| `ForceOffline` | Force a user offline. | `userId` required. | +| `ImportUser` | Import users from an uploaded file. | `fileId` required, optional `lang`. | + +## Connect RPC Examples + +Replace `mbs-capset` and `mbs-instance` with your OctoBus capset and instance IDs. The examples use sanitized hostnames and placeholder secrets only. + +### GetUsers + +```http +POST http://127.0.0.1:9000/capsets/mbs-capset/connect/mbs-instance/zhizhangyi.mbs.UserManagement/GetUsers +Content-Type: application/json + +{ + "index": 0, + "size": 20, + "orderCode": 0, + "orderType": 1, + "condition": { + "deptId": "1", + "keyWord": "", + "state": 0, + "isMdm": 0 + } +} +``` + +`condition.deptId` is required. Valid falsy filter values such as `state: 0` and `isMdm: 0` are preserved. + +### AddUser + +```http +POST http://127.0.0.1:9000/capsets/mbs-capset/connect/mbs-instance/zhizhangyi.mbs.UserManagement/AddUser +Content-Type: application/json + +{ + "userName": "Example User", + "loginName": "example.user", + "deptId": "1", + "password": "<3des-encrypted-password>", + "phoneNumber": "13800000000", + "email": "user@example.com", + "userSource": 0, + "state": 1, + "isMdm": 0 +} +``` + +`password` must already be encrypted in the format required by MBS. Empty numeric fields such as `state`, `isMdm`, and `weight` are treated as omitted instead of being converted to `0`. + +### UpdUser + +```http +POST http://127.0.0.1:9000/capsets/mbs-capset/connect/mbs-instance/zhizhangyi.mbs.UserManagement/UpdUser +Content-Type: application/json + +{ + "userId": "", + "userName": "Example User", + "deptId": "1", + "email": "user@example.com" +} +``` + +`userId`, `userName`, and `deptId` are required. + +### DelUsers + +```http +POST http://127.0.0.1:9000/capsets/mbs-capset/connect/mbs-instance/zhizhangyi.mbs.UserManagement/DelUsers +Content-Type: application/json + +{ + "type": 0, + "userIds": ["", ""] +} +``` + +For condition mode, set `type` to `1` and provide `condition`: + +```json +{ + "type": 1, + "condition": { + "deptId": "1", + "status": 0, + "isMdm": 0 + } +} +``` + +### StateUsers + +```http +POST http://127.0.0.1:9000/capsets/mbs-capset/connect/mbs-instance/zhizhangyi.mbs.UserManagement/StateUsers +Content-Type: application/json + +{ + "type": 0, + "state": "1", + "userIds": [""] +} +``` + +`state` is required and is not defaulted. Use `"1"` to enable and `"0"` to disable according to the MBS API semantics. + +### UpdUserPwd + +```http +POST http://127.0.0.1:9000/capsets/mbs-capset/connect/mbs-instance/zhizhangyi.mbs.UserManagement/UpdUserPwd +Content-Type: application/json + +{ + "version": "v2", + "loginName": "example.user", + "oldPwd": "<3des-encrypted-old-password>", + "newPwd": "<3des-encrypted-new-password>" +} +``` + +`version` is restricted to `v1` or `v2`. Invalid values are rejected before constructing the upstream path. + +## Error Handling + +HTTP errors from MBS are mapped to gRPC errors. Upstream HTTP response bodies are truncated before being included in error messages, so callers do not receive full upstream debug output or sensitive error details. + +## Local Testing + +Run service tests with: + +```bash +node --test services/zhizhangyi__mbs/test/zhizhangyi-mbs.test.js +npm --prefix services test -- --service-dir zhizhangyi__mbs +``` + +The test files use mocked HTTP responses and sanitized example values. diff --git a/services/zhizhangyi__mbs/bin/zhizhangyi-mbs.js b/services/zhizhangyi__mbs/bin/zhizhangyi-mbs.js new file mode 100755 index 00000000..508272f0 --- /dev/null +++ b/services/zhizhangyi__mbs/bin/zhizhangyi-mbs.js @@ -0,0 +1,7 @@ +#!/usr/bin/env node + +import { runServiceMain } from "@chaitin-ai/octobus-sdk"; + +import { service } from "../src/service.js"; + +runServiceMain(service); diff --git a/services/zhizhangyi__mbs/config.schema.json b/services/zhizhangyi__mbs/config.schema.json new file mode 100644 index 00000000..ad7646c5 --- /dev/null +++ b/services/zhizhangyi__mbs/config.schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": true, + "properties": { + "endpoint": { + "type": "string", + "description": "MBS REST API base URL. Example: https://mbs.example.com:9074" + }, + "baseUrl": { + "type": "string", + "description": "Legacy alias for endpoint." + }, + "skipTlsVerify": { + "type": "boolean", + "default": false, + "description": "Skip TLS certificate verification. Enable only when the target MBS endpoint uses a trusted self-signed certificate." + }, + "timeoutMs": { + "type": "integer", + "minimum": 1, + "default": 30000, + "description": "HTTP timeout in milliseconds." + } + } +} diff --git a/services/zhizhangyi__mbs/package.json b/services/zhizhangyi__mbs/package.json new file mode 100644 index 00000000..379063ac --- /dev/null +++ b/services/zhizhangyi__mbs/package.json @@ -0,0 +1,20 @@ +{ + "name": "zhizhangyi-mbs", + "version": "0.0.0", + "private": true, + "type": "module", + "files": [ + "bin/zhizhangyi-mbs.js", + "config.schema.json", + "secret.schema.json", + "service.json", + "proto", + "src" + ], + "bin": { + "zhizhangyi-mbs": "bin/zhizhangyi-mbs.js" + }, + "dependencies": { + "@chaitin-ai/octobus-sdk": "^0.6.0" + } +} diff --git a/services/zhizhangyi__mbs/proto/zhizhangyi_mbs.proto b/services/zhizhangyi__mbs/proto/zhizhangyi_mbs.proto new file mode 100644 index 00000000..454de278 --- /dev/null +++ b/services/zhizhangyi__mbs/proto/zhizhangyi_mbs.proto @@ -0,0 +1,353 @@ +syntax = "proto3"; + +package zhizhangyi.mbs; + +import "google/protobuf/struct.proto"; + +// ============================================================================= +// MBS (Mobile Security Management Platform) User Management API +// Vendor: 北京指掌易科技有限公司 +// Base URL: https://{host}:9074/uusafe/mos/thirdaccess/rest/opt/{version}/{operator} +// Auth: MD5(appkey + param1 + param2 + ... + secretkey), 拼接不含 "+" +// ============================================================================= + +service UserManagement { + // 6.2.1 用户列表(分页/筛选) + rpc GetUsers(GetUsersRequest) returns (GetUsersResponse); + + // 6.2.2 新增用户 + rpc AddUser(AddUserRequest) returns (AddUserResponse); + + // 6.2.3 编辑用户 + rpc UpdUser(UpdUserRequest) returns (UpdUserResponse); + + // 6.2.4 用户详情 + rpc DetailUser(DetailUserRequest) returns (DetailUserResponse); + + // 6.2.5 删除用户 + rpc DelUsers(DelUsersRequest) returns (DelUsersResponse); + + // 6.2.6 启停用户 + rpc StateUsers(StateUsersRequest) returns (StateUsersResponse); + + // 6.2.7 账号校验 + rpc CheckLoginName(CheckLoginNameRequest) returns (CheckLoginNameResponse); + + // 6.2.8 根据手机号获取用户 + rpc GetUserByPhone(GetUserByPhoneRequest) returns (GetUserByPhoneResponse); + + // 6.2.9 / 6.2.10 修改密码 + rpc UpdUserPwd(UpdUserPwdRequest) returns (UpdUserPwdResponse); + + // 6.2.11 强制下线 + rpc ForceOffline(ForceOfflineRequest) returns (ForceOfflineResponse); + + // 6.2.12 导入用户 + rpc ImportUser(ImportUserRequest) returns (ImportUserResponse); +} + +// ============================================================================= +// Common / Shared Messages +// ============================================================================= + +// 扩展字段键值对 +message ExAttrVo { + string attr_key = 1; + string attr_value = 2; +} + +// 查询条件 map +message GetUsersCondition { + string key_word = 1; // 关键字(用户名/姓名/手机号/工号/邮箱) + string state = 2; // 激活状态 -1未激活 0停用 1激活(逗号分隔多选) + string is_mdm = 3; // 是否开启设备管控 0未开启 1开启(逗号分隔多选) + string dept_id = 4; // 部门ID +} + +// 删除/启停条件 map +message BatchCondition { + string key_word = 1; + string status = 2; + string is_mdm = 3; + string dept_id = 4; +} + +// 用户基本信息(列表项) +message UserInfo { + string user_id = 1; + string user_name = 2; + string login_name = 3; + string phone_number = 4; + string email = 5; + string employee_number = 6; + string dept_id = 7; + string dept_name = 8; + int32 device_count = 9; + int32 is_mdm = 10; + int32 state = 11; + int32 is_admin = 12; + int32 user_source = 13; + int32 status = 14; + int32 weight = 15; + repeated ExAttrVo attrs = 16; + string dept_full_id = 17; + string dept_full_path = 18; + string job = 19; + string mobile = 20; + string address = 21; + string organization = 22; +} + +// 用户详情 +message UserDetail { + string user_id = 1; + string user_name = 2; + string login_name = 3; + string dept_id = 4; + string dept_name = 5; + string phone_number = 6; + string job = 7; + string employee_number = 8; + string address = 9; + string mobile = 10; + string email = 11; + string organization = 12; + int32 is_mdm = 13; + int32 state = 14; + int32 weight = 15; + string icon_file_id = 16; + repeated ExAttrVo attrs = 17; +} + +// ============================================================================= +// 6.2.1 GetUsers - 用户列表 +// ============================================================================= +message GetUsersRequest { + int32 index = 1; // 页号(0起始) + int32 size = 2; // 每页数量(默认10,最大5000) + int32 order_code = 3; // 排序字段 0用户名 1姓名 2工号 3用户状态 + int32 order_type = 4; // 排序类型 0 asc 1 desc + GetUsersCondition condition = 5; // 查询条件 + string org_code = 6; // 机构编码 + string appkey = 7; // 接入账号 + string sign = 8; // MD5签名(自动计算) +} + +message GetUsersResponse { + int32 code = 1; + string msg = 2; + GetUsersData data = 3; + string time_stamp = 4; +} + +message GetUsersData { + int32 total = 1; + repeated UserInfo user_infos = 2; +} + +// ============================================================================= +// 6.2.2 AddUser - 新增用户 +// ============================================================================= +message AddUserRequest { + string user_name = 1; // 用户姓名(必填) + string login_name = 2; // 登录名(必填) + string dept_id = 3; // 部门ID(必填) + string password = 4; // MBS要求的3DES加密后密码串(必填,由调用方提供) + string phone_number = 5; + string job = 6; + string employee_number = 7; + string address = 8; + string mobile = 9; + string email = 10; + string organization = 11; + int32 user_source = 12; // 0本地 2第三方(必填) + int32 is_mdm = 13; // 是否开启设备管理 0否 1是 + int32 state = 14; // 1启用 0停用 -1未激活 + int32 weight = 15; + repeated ExAttrVo attrs = 16; + string org_code = 17; + string appkey = 18; + string sign = 19; +} + +message AddUserResponse { + int32 code = 1; + string msg = 2; + google.protobuf.Value data = 3; // 通常为空对象 +} + +// ============================================================================= +// 6.2.3 UpdUser - 编辑用户 +// ============================================================================= +message UpdUserRequest { + string user_id = 1; // 用户ID(必填) + string user_name = 2; // 用户姓名(必填) + string login_name = 3; // 登录名(不可修改账号) + string dept_id = 4; // 部门ID(必填) + string phone_number = 5; + string job = 6; + string employee_number = 7; + string address = 8; + string mobile = 9; + string email = 10; + string organization = 11; + int32 is_mdm = 12; + int32 weight = 13; + repeated ExAttrVo attrs = 14; + string org_code = 15; + string appkey = 16; + string sign = 17; +} + +message UpdUserResponse { + int32 code = 1; + string msg = 2; + google.protobuf.Value data = 3; +} + +// ============================================================================= +// 6.2.4 DetailUser - 用户详情 +// ============================================================================= +message DetailUserRequest { + string user_id = 1; + string org_code = 2; + string appkey = 3; + string sign = 4; +} + +message DetailUserResponse { + int32 code = 1; + string msg = 2; + UserDetail data = 3; +} + +// ============================================================================= +// 6.2.5 DelUsers - 删除用户 +// ============================================================================= +message DelUsersRequest { + repeated string user_ids = 1; // 用户ID列表 + int32 type = 2; // 0指定ID删除 1指定条件删除 + BatchCondition condition = 3; // type=1时的查询条件 + string org_code = 4; + string appkey = 5; + string sign = 6; +} + +message DelUsersResponse { + int32 code = 1; + string msg = 2; + google.protobuf.Value data = 3; +} + +// ============================================================================= +// 6.2.6 StateUsers - 启停用户 +// ============================================================================= +message StateUsersRequest { + repeated string user_ids = 1; + int32 type = 2; // 0指定ID 1指定条件 + string state = 3; // 0停用 1启用 + BatchCondition condition = 4; + string org_code = 5; + string appkey = 6; + string sign = 7; +} + +message StateUsersResponse { + int32 code = 1; + string msg = 2; + google.protobuf.Value data = 3; +} + +// ============================================================================= +// 6.2.7 CheckLoginName - 账号校验 +// ============================================================================= +message CheckLoginNameRequest { + string login_name = 1; + string org_code = 2; + string appkey = 3; + string sign = 4; +} + +message CheckLoginNameResponse { + int32 code = 1; + string msg = 2; + google.protobuf.Value data = 3; +} + +// ============================================================================= +// 6.2.8 GetUserByPhone - 根据手机号获取用户 +// ============================================================================= +message GetUserByPhoneRequest { + string phone = 1; + string org_code = 2; + string appkey = 3; + string sign = 4; +} + +message GetUserByPhoneResponse { + int32 code = 1; + string msg = 2; + repeated PhoneUserInfo data = 3; +} + +message PhoneUserInfo { + string user_id = 1; + string user_name = 2; + string login_name = 3; + string phone_number = 4; + string email = 5; +} + +// ============================================================================= +// 6.2.9 / 6.2.10 UpdUserPwd - 修改密码 +// ============================================================================= +message UpdUserPwdRequest { + string user_id = 1; // v1版本必填 + string password = 2; // MBS要求的3DES加密后新密码串(v1,由调用方提供) + string old_pwd = 3; // MBS要求的3DES加密后原密码串(v2,由调用方提供) + string new_pwd = 4; // MBS要求的3DES加密后新密码串(v2,由调用方提供) + string login_name = 5; // v2版本必填 + string version = 6; // "v1" 管理员修改 / "v2" 用户自服务 + string org_code = 7; + string appkey = 8; + string sign = 9; +} + +message UpdUserPwdResponse { + int32 code = 1; + string msg = 2; + google.protobuf.Value data = 3; +} + +// ============================================================================= +// 6.2.11 ForceOffline - 强制下线 +// ============================================================================= +message ForceOfflineRequest { + string user_id = 1; + string org_code = 2; + string appkey = 3; + string sign = 4; +} + +message ForceOfflineResponse { + int32 code = 1; + string msg = 2; + google.protobuf.Value data = 3; +} + +// ============================================================================= +// 6.2.12 ImportUser - 导入用户 +// ============================================================================= +message ImportUserRequest { + int32 lang = 1; // 0中文 1英文 2简体中文 + string file_id = 2; // 文件ID + string org_code = 3; + string appkey = 4; + string sign = 5; +} + +message ImportUserResponse { + int32 code = 1; + string msg = 2; + google.protobuf.Value data = 3; +} \ No newline at end of file diff --git a/services/zhizhangyi__mbs/secret.schema.json b/services/zhizhangyi__mbs/secret.schema.json new file mode 100644 index 00000000..444f7b8e --- /dev/null +++ b/services/zhizhangyi__mbs/secret.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": true, + "required": ["appkey", "secretkey", "orgCode"], + "properties": { + "appkey": { + "type": "string", + "description": "MBS third-party access appkey." + }, + "secretkey": { + "type": "string", + "description": "MBS third-party access secretkey paired with appkey." + }, + "orgCode": { + "type": "string", + "description": "Organization code (orgCode) for MBS tenant." + } + } +} diff --git a/services/zhizhangyi__mbs/service.json b/services/zhizhangyi__mbs/service.json new file mode 100644 index 00000000..9fb14081 --- /dev/null +++ b/services/zhizhangyi__mbs/service.json @@ -0,0 +1,69 @@ +{ + "schema": "chaitin.octobus.service.v1", + "name": "zhizhangyi-mbs", + "displayName": "MBS Mobile Security Management", + "description": "OctoBus package for 指掌易 MBS (Mobile Security) User Management APIs. Provides CRUD for users, password management, account validation, force offline, and bulk import.", + "runtime": { + "mode": "long-running" + }, + "proto": { + "roots": [ + "proto" + ], + "files": [ + "proto/zhizhangyi_mbs.proto" + ] + }, + "configSchema": "config.schema.json", + "secretSchema": "secret.schema.json", + "sdk": { + "cli": { + "commands": { + "zhizhangyi.mbs.UserManagement/GetUsers": { + "name": "get-users", + "description": "List MBS users with pagination, filtering, and sorting." + }, + "zhizhangyi.mbs.UserManagement/AddUser": { + "name": "add-user", + "description": "Create a new MBS user." + }, + "zhizhangyi.mbs.UserManagement/UpdUser": { + "name": "update-user", + "description": "Update an existing MBS user." + }, + "zhizhangyi.mbs.UserManagement/DetailUser": { + "name": "detail-user", + "description": "Get MBS user detail by user ID." + }, + "zhizhangyi.mbs.UserManagement/DelUsers": { + "name": "delete-users", + "description": "Delete MBS users by ID or condition." + }, + "zhizhangyi.mbs.UserManagement/StateUsers": { + "name": "state-users", + "description": "Enable or disable MBS users." + }, + "zhizhangyi.mbs.UserManagement/CheckLoginName": { + "name": "check-login-name", + "description": "Check if a login name is available." + }, + "zhizhangyi.mbs.UserManagement/GetUserByPhone": { + "name": "get-user-by-phone", + "description": "Find MBS users by phone number." + }, + "zhizhangyi.mbs.UserManagement/UpdUserPwd": { + "name": "update-user-password", + "description": "Change user password (admin or self-service)." + }, + "zhizhangyi.mbs.UserManagement/ForceOffline": { + "name": "force-offline", + "description": "Force a user to be logged out." + }, + "zhizhangyi.mbs.UserManagement/ImportUser": { + "name": "import-user", + "description": "Import users from an uploaded file." + } + } + } + } +} diff --git a/services/zhizhangyi__mbs/src/service.js b/services/zhizhangyi__mbs/src/service.js new file mode 100644 index 00000000..d69d1c52 --- /dev/null +++ b/services/zhizhangyi__mbs/src/service.js @@ -0,0 +1,7 @@ +import { defineService } from "@chaitin-ai/octobus-sdk"; + +import { handlers } from "./zhizhangyi-mbs.js"; + +export { handlers } from "./zhizhangyi-mbs.js"; + +export const service = defineService({ handlers }); diff --git a/services/zhizhangyi__mbs/src/zhizhangyi-mbs.js b/services/zhizhangyi__mbs/src/zhizhangyi-mbs.js new file mode 100644 index 00000000..83b09036 --- /dev/null +++ b/services/zhizhangyi__mbs/src/zhizhangyi-mbs.js @@ -0,0 +1,282 @@ +// MBS User Management API Proxy - Part 1: helpers + first 6 handlers +import { GrpcError, grpcStatus } from '@chaitin-ai/octobus-sdk'; +import crypto from 'node:crypto'; + +const DEF_TO = 30000; +const BASE = '/uusafe/mos/thirdaccess/rest/opt'; + +const has = (o, k) => Object.prototype.hasOwnProperty.call(o ?? {}, k); +const first = (...vs) => vs.find((v) => v !== undefined && v !== null); +const merge = (ctx = {}) => ({ ...(ctx?.config ?? {}), ...(ctx?.secret ?? {}), ...(ctx?.bindings ?? {}) }); +const normUrl = (u) => /^https?:\/\//i.test(String(u || '').trim()) ? String(u).trim().replace(/\/$/, '') : null; +const md5 = (s) => crypto.createHash('md5').update(s, 'utf8').digest('hex'); +const signCalc = (sk, ...ps) => md5(ps.map((p) => (p === undefined || p === null ? '' : String(p))).join('') + String(sk || '')); +const arr = (v) => Array.isArray(v) ? v : []; +const str = (v) => (v === undefined || v === null || v === '') ? undefined : String(v); +const num = (v) => (v === undefined || v === null || v === '') ? undefined : Number(v); +const gc = (c) => ({ INVALID_ARGUMENT: grpcStatus.INVALID_ARGUMENT, FAILED_PRECONDITION: grpcStatus.FAILED_PRECONDITION, PERMISSION_DENIED: grpcStatus.PERMISSION_DENIED, UNAVAILABLE: grpcStatus.UNAVAILABLE, DEADLINE_EXCEEDED: grpcStatus.DEADLINE_EXCEEDED })[c] ?? grpcStatus.UNKNOWN; +const er = (c, m) => { const e = new GrpcError(gc(c), c + ': ' + m); e.legacyCode = c; return e; }; +const doFetch = async (url, init, to, st) => { const tls = st ? { insecureSkipVerify: true, tlsInsecureSkipVerify: true } : {}; try { return await fetch(url, { ...init, timeoutMs: to, ...tls }); } catch (e) { throw er('UNAVAILABLE', e?.cause?.message || e?.message || 'fetch failed'); } }; +const rdJson = async (res) => { const t = await res.text(); if (!res.ok) { const safe = t.length > 200 ? t.slice(0, 200) + '...' : t; throw er(res.status === 401 || res.status === 403 ? 'PERMISSION_DENIED' : res.status >= 400 && res.status < 500 ? 'FAILED_PRECONDITION' : 'UNAVAILABLE', 'http ' + res.status + ': ' + safe); } if (!t.trim()) return {}; try { return JSON.parse(t); } catch { throw er('UNKNOWN', 'not JSON'); } }; +const check = (j) => { if (j.code !== undefined && j.code !== 0) throw er('FAILED_PRECONDITION', 'MBS code=' + j.code + ': ' + (j.msg || '')); }; +const buildCond = (cond) => { const c = {}; const kw = first(cond?.key_word, cond?.keyWord); if (kw !== undefined) c.keyWord = kw; if (cond?.status !== undefined && cond?.status !== null) c.status = cond.status; const im = first(cond?.is_mdm, cond?.isMdm); if (im !== undefined) c.isMdm = im; const did = first(cond?.dept_id, cond?.deptId); if (did !== undefined) c.deptId = did; return c; }; +const toValue = (value) => { + if (value === undefined || value === null) return { nullValue: 'NULL_VALUE' }; + if (typeof value === 'string') return { stringValue: value }; + if (typeof value === 'number') return Number.isFinite(value) ? { numberValue: value } : { stringValue: String(value) }; + if (typeof value === 'boolean') return { boolValue: value }; + if (Array.isArray(value)) return { listValue: { values: value.map((item) => toValue(item)) } }; + if (typeof value === 'object') { + const fields = {}; + for (const [k, v] of Object.entries(value)) fields[k] = toValue(v); + return { structValue: { fields } }; + } + return { stringValue: String(value) }; +}; + +const mapUser = (it) => ({ user_id: it?.userId ?? '', user_name: it?.userName ?? '', login_name: it?.loginName ?? '', phone_number: it?.phoneNumber ?? '', email: it?.email ?? '', employee_number: it?.employeeNumber ?? '', dept_id: it?.deptId ?? '', dept_name: it?.deptName ?? '', device_count: it?.deviceCount ?? 0, is_mdm: it?.isMdm ?? 0, state: it?.state ?? 0, is_admin: it?.isAdmin ?? 0, user_source: it?.userSource ?? 0, status: it?.status ?? 1, weight: it?.weight ?? 0, attrs: arr(it?.attrs).map((a) => ({ attr_key: a?.attrKey ?? '', attr_value: a?.attrValue ?? '' })), dept_full_id: it?.deptFullId ?? '', dept_full_path: it?.deptFullPath ?? '', job: it?.job ?? '', mobile: it?.mobile ?? '', address: it?.address ?? '', organization: it?.organization ?? '' }); +const mapDetail = (it) => ({ user_id: it?.userId ?? '', user_name: it?.userName ?? '', login_name: it?.loginName ?? '', dept_id: it?.deptId ?? '', dept_name: it?.deptName ?? '', phone_number: it?.phoneNumber ?? '', job: it?.job ?? '', employee_number: it?.employeeNumber ?? '', address: it?.address ?? '', mobile: it?.mobile ?? '', email: it?.email ?? '', organization: it?.organization ?? '', is_mdm: it?.isMdm ?? 0, state: it?.state ?? 0, weight: it?.weight ?? 0, icon_file_id: it?.iconFileId ?? '', attrs: arr(it?.attrs).map((a) => ({ attr_key: a?.attrKey ?? '', attr_value: a?.attrValue ?? '' })) }); +const mapPhone = (it) => ({ user_id: it?.userId ?? '', user_name: it?.userName ?? '', login_name: it?.loginName ?? '', phone_number: it?.phoneNumber ?? '', email: it?.email ?? '' }); + +const M = { + GetUsers: 'zhizhangyi.mbs.UserManagement/GetUsers', + AddUser: 'zhizhangyi.mbs.UserManagement/AddUser', + UpdUser: 'zhizhangyi.mbs.UserManagement/UpdUser', + DetailUser: 'zhizhangyi.mbs.UserManagement/DetailUser', + DelUsers: 'zhizhangyi.mbs.UserManagement/DelUsers', + StateUsers: 'zhizhangyi.mbs.UserManagement/StateUsers', + CheckLoginName: 'zhizhangyi.mbs.UserManagement/CheckLoginName', + GetUserByPhone: 'zhizhangyi.mbs.UserManagement/GetUserByPhone', + UpdUserPwd: 'zhizhangyi.mbs.UserManagement/UpdUserPwd', + ForceOffline: 'zhizhangyi.mbs.UserManagement/ForceOffline', + ImportUser: 'zhizhangyi.mbs.UserManagement/ImportUser', +}; + +export function rpcdef(ctx) { + const b = merge(ctx); + const baseUrl = normUrl(b.endpoint || b.baseUrl || ''); + const to = ctx?.limits?.timeoutMs || b.timeoutMs || DEF_TO; + const skipTls = Boolean(b.skipTlsVerify); + const ak = b.appkey || ''; + const sk = b.secretkey || ''; + const ocDef = b.orgCode || ''; + + const post = async (path, body) => { + if (!baseUrl) throw er('INVALID_ARGUMENT', 'endpoint/baseUrl required'); + const r = await doFetch(baseUrl + path, { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8' }, body: JSON.stringify(body) }, to, skipTls); + const j = await rdJson(r); check(j); return j; + }; + + // 6.2.1 GetUsers + const goGetUsers = async (req) => { + const oc = first(req?.org_code, req?.orgCode) || ocDef; + const apk = first(req?.appkey) || ak; + const idx = first(req?.index) ?? 0; const sz = first(req?.size) ?? 10; + const odc = first(req?.order_code, req?.orderCode) ?? 0; const odt = first(req?.order_type, req?.orderType) ?? 1; + const did = first(req?.condition?.dept_id, req?.condition?.deptId); + if (!did) throw er('INVALID_ARGUMENT', 'dept_id required'); + const kw = first(req?.condition?.key_word, req?.condition?.keyWord) || ''; + const st = first(req?.condition?.state) ?? ''; const md = first(req?.condition?.is_mdm, req?.condition?.isMdm) ?? ''; + const sg = first(req?.sign) || signCalc(sk, apk, oc, idx, sz, odc, odt, kw, st, md, did); + const cond = { deptId: did, keyWord: kw }; if (st !== '') cond.state = st; if (md !== '') cond.isMdm = md; + const j = await post(BASE + '/v1/getUsers', { index: Number(idx), size: Number(sz), orderCode: Number(odc), orderType: Number(odt), condition: cond, orgCode: oc, appkey: apk, sign: sg }); + const d = j?.data || {}; + return { code: j?.code ?? 0, msg: j?.msg ?? '', data: { total: d.total ?? 0, user_infos: arr(d.userInfos).map(mapUser) }, time_stamp: j?.timeStamp ?? '' }; + }; + + // 6.2.2 AddUser + const goAddUser = async (req) => { + const oc = first(req?.org_code, req?.orgCode) || ocDef; const apk = first(req?.appkey) || ak; + const un = first(req?.user_name, req?.userName) || ''; const ln = first(req?.login_name, req?.loginName) || ''; + const did = first(req?.dept_id, req?.deptId) || ''; const encryptedPw = first(req?.password) || ''; + if (!un) throw er('INVALID_ARGUMENT', 'user_name required'); if (!ln) throw er('INVALID_ARGUMENT', 'login_name required'); if (!did) throw er('INVALID_ARGUMENT', 'dept_id required'); if (!encryptedPw) throw er('INVALID_ARGUMENT', 'password required as 3DES-encrypted value'); + const sg = first(req?.sign) || signCalc(sk, apk, oc, un, ln, did, encryptedPw); + const body = { userName: un, loginName: ln, deptId: did, password: encryptedPw, userSource: Number(first(req?.user_source, req?.userSource) ?? 0), orgCode: oc, appkey: apk, sign: sg }; + const sm = { phone_number: 'phoneNumber', job: 'job', employee_number: 'employeeNumber', address: 'address', mobile: 'mobile', email: 'email', organization: 'organization' }; + for (const [k, jk] of Object.entries(sm)) { const v = str(first(req?.[k])); if (v !== undefined) body[jk] = v; } + for (const k of ['is_mdm', 'state', 'weight']) { const v = num(first(req?.[k])); if (v !== undefined) body[k === 'is_mdm' ? 'isMdm' : k] = v; } + if (arr(req?.attrs).length > 0) body.attrs = req.attrs.map((a) => ({ attrKey: a?.attr_key ?? '', attrValue: a?.attr_value ?? '' })); + const j = await post(BASE + '/v1/addUser', body); + return { code: j?.code ?? 0, msg: j?.msg ?? '', data: toValue(j?.data ?? null) }; + }; + + // 6.2.3 UpdUser + const goUpdUser = async (req) => { + const oc = first(req?.org_code, req?.orgCode) || ocDef; const apk = first(req?.appkey) || ak; + const uid = first(req?.user_id, req?.userId) || ''; const un = first(req?.user_name, req?.userName) || ''; + if (!uid) throw er('INVALID_ARGUMENT', 'user_id required'); if (!un) throw er('INVALID_ARGUMENT', 'user_name required'); + const ln = first(req?.login_name, req?.loginName) || ''; const did = first(req?.dept_id, req?.deptId) || ''; + if (!did) throw er('INVALID_ARGUMENT', 'dept_id required'); + const sg = first(req?.sign) || signCalc(sk, apk, oc, uid, un, ln, did); + const body = { userId: uid, userName: un, loginName: ln, deptId: did, orgCode: oc, appkey: apk, sign: sg }; + const sm = { phone_number: 'phoneNumber', job: 'job', employee_number: 'employeeNumber', address: 'address', mobile: 'mobile', email: 'email', organization: 'organization' }; + for (const [k, jk] of Object.entries(sm)) { const v = str(first(req?.[k])); if (v !== undefined) body[jk] = v; } + for (const k of ['is_mdm', 'weight']) { const v = num(first(req?.[k])); if (v !== undefined) body[k === 'is_mdm' ? 'isMdm' : k] = v; } + if (arr(req?.attrs).length > 0) body.attrs = req.attrs.map((a) => ({ attrKey: a?.attr_key ?? '', attrValue: a?.attr_value ?? '' })); + const j = await post(BASE + '/v1/updUser', body); + return { code: j?.code ?? 0, msg: j?.msg ?? '', data: toValue(j?.data ?? null) }; + }; + + // 6.2.4 DetailUser + const goDetailUser = async (req) => { + const oc = first(req?.org_code, req?.orgCode) || ocDef; const apk = first(req?.appkey) || ak; + const uid = first(req?.user_id, req?.userId) || ''; if (!uid) throw er('INVALID_ARGUMENT', 'user_id required'); + const sg = first(req?.sign) || signCalc(sk, apk, oc, uid); + const j = await post(BASE + '/v1/detailUser', { userId: uid, orgCode: oc, appkey: apk, sign: sg }); + return { code: j?.code ?? 0, msg: j?.msg ?? '', data: j?.data ? mapDetail(j.data) : null }; + }; + + // 6.2.5 DelUsers + const goDelUsers = async (req) => { + const oc = first(req?.org_code, req?.orgCode) || ocDef; const apk = first(req?.appkey) || ak; + const tp = Number(first(req?.type) ?? 0); const uids = arr(first(req?.user_ids, req?.userIds)); + const did = first(req?.condition?.dept_id, req?.condition?.deptId) || ''; + if (tp === 0 && uids.length === 0) throw er('INVALID_ARGUMENT', 'user_ids required for type=0'); + const sg = first(req?.sign) || signCalc(sk, apk, oc, uids.join(','), tp, did); + const body = { type: tp, orgCode: oc, appkey: apk, sign: sg }; + if (tp === 0) { body.userIds = uids; } else { const c = buildCond(req?.condition); if (Object.keys(c).length > 0) body.condition = c; } + const j = await post(BASE + '/v1/delUsers', body); + return { code: j?.code ?? 0, msg: j?.msg ?? '', data: toValue(j?.data ?? null) }; + }; + + // 6.2.6 StateUsers + const goStateUsers = async (req) => { + const oc = first(req?.org_code, req?.orgCode) || ocDef; const apk = first(req?.appkey) || ak; + const tp = Number(first(req?.type) ?? 0); const st = first(req?.state); + if (st === undefined || st === null) throw er('INVALID_ARGUMENT', 'state required'); + const uids = arr(first(req?.user_ids, req?.userIds)); + const did = first(req?.condition?.dept_id, req?.condition?.deptId) || ''; + if (tp === 0 && uids.length === 0) throw er('INVALID_ARGUMENT', 'user_ids required for type=0'); + const sg = first(req?.sign) || signCalc(sk, apk, oc, uids.join(','), tp, st, did); + const body = { type: tp, state: st, orgCode: oc, appkey: apk, sign: sg }; + if (tp === 0) { body.userIds = uids; } else { const c = buildCond(req?.condition); if (Object.keys(c).length > 0) body.condition = c; } + const j = await post(BASE + '/v1/stateUsers', body); + return { code: j?.code ?? 0, msg: j?.msg ?? '', data: toValue(j?.data ?? null) }; + }; + + // 6.2.7 CheckLoginName + const goCheckLoginName = async (req) => { + const oc = first(req?.org_code, req?.orgCode) || ocDef; const apk = first(req?.appkey) || ak; + const ln = first(req?.login_name, req?.loginName) || ''; if (!ln) throw er('INVALID_ARGUMENT', 'login_name required'); + const sg = first(req?.sign) || signCalc(sk, apk, oc, ln); + const j = await post(BASE + '/v1/checkLoginName', { loginName: ln, orgCode: oc, appkey: apk, sign: sg }); + return { code: j?.code ?? 0, msg: j?.msg ?? '', data: toValue(j?.data ?? null) }; + }; + + // 6.2.8 GetUserByPhone + const goGetUserByPhone = async (req) => { + const oc = first(req?.org_code, req?.orgCode) || ocDef; const apk = first(req?.appkey) || ak; + const ph = first(req?.phone) || ''; if (!ph) throw er('INVALID_ARGUMENT', 'phone required'); + const sg = first(req?.sign) || signCalc(sk, apk, oc, ph); + const j = await post(BASE + '/v1/getUserByPhone', { phone: ph, orgCode: oc, appkey: apk, sign: sg }); + return { code: j?.code ?? 0, msg: j?.msg ?? '', data: arr(j?.data).map(mapPhone) }; + }; + + // 6.2.9/6.2.10 UpdUserPwd + const goUpdUserPwd = async (req) => { + const oc = first(req?.org_code, req?.orgCode) || ocDef; const apk = first(req?.appkey) || ak; + const ver = first(req?.version) || 'v1'; + if (ver !== 'v1' && ver !== 'v2') throw er('INVALID_ARGUMENT', 'version must be v1 or v2'); + let body; + if (ver === 'v2') { + const ln = first(req?.login_name, req?.loginName) || ''; const encryptedNewPwd = first(req?.new_pwd, req?.newPwd) || ''; + if (!ln) throw er('INVALID_ARGUMENT', 'login_name required for v2'); if (!encryptedNewPwd) throw er('INVALID_ARGUMENT', 'new_pwd required as 3DES-encrypted value for v2'); + const sg = first(req?.sign) || signCalc(sk, apk, oc, ln, encryptedNewPwd); + body = { loginName: ln, newPwd: encryptedNewPwd, orgCode: oc, appkey: apk, sign: sg }; + const encryptedOldPwd = first(req?.old_pwd, req?.oldPwd); if (encryptedOldPwd) body.oldPwd = encryptedOldPwd; + } else { + const uid = first(req?.user_id, req?.userId) || ''; const encryptedPw = first(req?.password) || ''; + if (!uid) throw er('INVALID_ARGUMENT', 'user_id required for v1'); if (!encryptedPw) throw er('INVALID_ARGUMENT', 'password required as 3DES-encrypted value for v1'); + const sg = first(req?.sign) || signCalc(sk, apk, oc, uid, encryptedPw); + body = { userId: uid, password: encryptedPw, orgCode: oc, appkey: apk, sign: sg }; + } + const j = await post(BASE + `/${ver}/updUserPwd`, body); + return { code: j?.code ?? 0, msg: j?.msg ?? '', data: toValue(j?.data ?? null) }; + }; + + // 6.2.11 ForceOffline + const goForceOffline = async (req) => { + const oc = first(req?.org_code, req?.orgCode) || ocDef; const apk = first(req?.appkey) || ak; + const uid = first(req?.user_id, req?.userId) || ''; if (!uid) throw er('INVALID_ARGUMENT', 'user_id required'); + const sg = first(req?.sign) || signCalc(sk, apk, oc, uid); + const j = await post(BASE + '/v1/forceOffline', { userId: uid, orgCode: oc, appkey: apk, sign: sg }); + return { code: j?.code ?? 0, msg: j?.msg ?? '', data: toValue(j?.data ?? null) }; + }; + + // 6.2.12 ImportUser + const goImportUser = async (req) => { + const oc = first(req?.org_code, req?.orgCode) || ocDef; const apk = first(req?.appkey) || ak; + const lang = first(req?.lang) ?? 0; const fid = first(req?.file_id, req?.fileId) || ''; + if (!fid) throw er('INVALID_ARGUMENT', 'file_id required'); + const sg = first(req?.sign) || signCalc(sk, apk, oc, lang, fid); + const j = await post(BASE + '/v1/importUser', { lang: Number(lang), fileId: fid, orgCode: oc, appkey: apk, sign: sg }); + return { code: j?.code ?? 0, msg: j?.msg ?? '', data: toValue(j?.data ?? null) }; + }; + + return { + [M.GetUsers]: async () => goGetUsers(ctx.req || {}), + [M.AddUser]: async () => goAddUser(ctx.req || {}), + [M.UpdUser]: async () => goUpdUser(ctx.req || {}), + [M.DetailUser]: async () => goDetailUser(ctx.req || {}), + [M.DelUsers]: async () => goDelUsers(ctx.req || {}), + [M.StateUsers]: async () => goStateUsers(ctx.req || {}), + [M.CheckLoginName]: async () => goCheckLoginName(ctx.req || {}), + [M.GetUserByPhone]: async () => goGetUserByPhone(ctx.req || {}), + [M.UpdUserPwd]: async () => goUpdUserPwd(ctx.req || {}), + [M.ForceOffline]: async () => goForceOffline(ctx.req || {}), + [M.ImportUser]: async () => goImportUser(ctx.req || {}), + }; +} + + +// Legacy handler wrapper +function wrapLegacyHandler(baseCtx, methodPath) { + return async function(reqOrCtx, maybeInnerCtx) { + var incoming = (reqOrCtx && typeof reqOrCtx === 'object') ? reqOrCtx : {}; + var callCtx = { + ...(baseCtx ?? {}), + ...incoming, + req: incoming.request ?? incoming.req ?? reqOrCtx ?? {}, + request: incoming.request ?? incoming.req ?? reqOrCtx ?? {}, + config: incoming.config ?? baseCtx?.config, + secret: incoming.secret ?? baseCtx?.secret, + metadata: incoming.metadata ?? baseCtx?.metadata, + meta: incoming.meta ?? baseCtx?.meta, + getMetadata: incoming.getMetadata ?? baseCtx?.getMetadata, + getMetadataAll: incoming.getMetadataAll ?? baseCtx?.getMetadataAll, + }; + return rpcdef(callCtx)[methodPath](); + }; +} + +function registerHandlers(ctx) { + return { + [M.GetUsers]: wrapLegacyHandler(ctx, M.GetUsers), + [M.AddUser]: wrapLegacyHandler(ctx, M.AddUser), + [M.UpdUser]: wrapLegacyHandler(ctx, M.UpdUser), + [M.DetailUser]: wrapLegacyHandler(ctx, M.DetailUser), + [M.DelUsers]: wrapLegacyHandler(ctx, M.DelUsers), + [M.StateUsers]: wrapLegacyHandler(ctx, M.StateUsers), + [M.CheckLoginName]: wrapLegacyHandler(ctx, M.CheckLoginName), + [M.GetUserByPhone]: wrapLegacyHandler(ctx, M.GetUserByPhone), + [M.UpdUserPwd]: wrapLegacyHandler(ctx, M.UpdUserPwd), + [M.ForceOffline]: wrapLegacyHandler(ctx, M.ForceOffline), + [M.ImportUser]: wrapLegacyHandler(ctx, M.ImportUser), + }; +} + +var sdkHandlers = registerHandlers({}); + +export var handlers = { + [M.GetUsers]: (ctx) => sdkHandlers[M.GetUsers](ctx), + [M.AddUser]: (ctx) => sdkHandlers[M.AddUser](ctx), + [M.UpdUser]: (ctx) => sdkHandlers[M.UpdUser](ctx), + [M.DetailUser]: (ctx) => sdkHandlers[M.DetailUser](ctx), + [M.DelUsers]: (ctx) => sdkHandlers[M.DelUsers](ctx), + [M.StateUsers]: (ctx) => sdkHandlers[M.StateUsers](ctx), + [M.CheckLoginName]: (ctx) => sdkHandlers[M.CheckLoginName](ctx), + [M.GetUserByPhone]: (ctx) => sdkHandlers[M.GetUserByPhone](ctx), + [M.UpdUserPwd]: (ctx) => sdkHandlers[M.UpdUserPwd](ctx), + [M.ForceOffline]: (ctx) => sdkHandlers[M.ForceOffline](ctx), + [M.ImportUser]: (ctx) => sdkHandlers[M.ImportUser](ctx), +}; diff --git a/services/zhizhangyi__mbs/test/zhizhangyi-mbs.test.js b/services/zhizhangyi__mbs/test/zhizhangyi-mbs.test.js new file mode 100644 index 00000000..653a92ac --- /dev/null +++ b/services/zhizhangyi__mbs/test/zhizhangyi-mbs.test.js @@ -0,0 +1,406 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { GrpcError, grpcStatus } from '@chaitin-ai/octobus-sdk'; + +import { rpcdef } from '../src/zhizhangyi-mbs.js'; + +const ADD_USER = 'zhizhangyi.mbs.UserManagement/AddUser'; +const DEL_USERS = 'zhizhangyi.mbs.UserManagement/DelUsers'; +const GET_USERS = 'zhizhangyi.mbs.UserManagement/GetUsers'; +const STATE_USERS = 'zhizhangyi.mbs.UserManagement/StateUsers'; +const UPD_USER = 'zhizhangyi.mbs.UserManagement/UpdUser'; +const UPD_USER_PWD = 'zhizhangyi.mbs.UserManagement/UpdUserPwd'; + +const originalFetch = globalThis.fetch; + +const buildCtx = (req) => ({ + config: { endpoint: 'https://mbs.example' }, + secret: { appkey: 'appkey', secretkey: 'secretkey', orgCode: 'org' }, + req, +}); + +test.afterEach(() => { + globalThis.fetch = originalFetch; +}); + +test('AddUser requires password before calling upstream', async () => { + let called = false; + globalThis.fetch = async () => { + called = true; + return { ok: true, status: 200, text: async () => '{"code":0}' }; + }; + + await assert.rejects( + () => rpcdef(buildCtx({ user_name: 'User', login_name: 'user', dept_id: 'dept' }))[ADD_USER](), + (err) => { + assert.ok(err instanceof GrpcError); + assert.equal(err.code, grpcStatus.INVALID_ARGUMENT); + assert.equal(err.legacyCode, 'INVALID_ARGUMENT'); + assert.match(err.message, /password required/); + return true; + }, + ); + + assert.equal(called, false); +}); + +test('GetUsers preserves falsy state and is_mdm filters', async () => { + let captured; + globalThis.fetch = async (url, init) => { + captured = { url, init }; + return { ok: true, status: 200, text: async () => '{"code":0,"data":{"total":0,"userInfos":[]}}' }; + }; + + await rpcdef(buildCtx({ condition: { dept_id: '1', state: 0, is_mdm: 0 } }))[GET_USERS](); + + assert.equal(captured.url, 'https://mbs.example/uusafe/mos/thirdaccess/rest/opt/v1/getUsers'); + assert.deepEqual(JSON.parse(captured.init.body).condition, { + deptId: '1', + keyWord: '', + state: 0, + isMdm: 0, + }); +}); + +test('GetUsers requires dept_id before calling upstream', async () => { + let called = false; + globalThis.fetch = async () => { + called = true; + return { ok: true, status: 200, text: async () => '{"code":0}' }; + }; + + await assert.rejects( + () => rpcdef(buildCtx({ condition: { state: 1 } }))[GET_USERS](), + (err) => { + assert.ok(err instanceof GrpcError); + assert.equal(err.code, grpcStatus.INVALID_ARGUMENT); + assert.equal(err.legacyCode, 'INVALID_ARGUMENT'); + assert.match(err.message, /dept_id required/); + return true; + }, + ); + + assert.equal(called, false); +}); + +test('HTTP errors expose only a bounded upstream body summary', async () => { + const body = 'x'.repeat(260); + globalThis.fetch = async () => ({ ok: false, status: 500, text: async () => body }); + + await assert.rejects( + () => rpcdef(buildCtx({ condition: { dept_id: '1' } }))[GET_USERS](), + (err) => { + assert.ok(err instanceof GrpcError); + assert.equal(err.code, grpcStatus.UNAVAILABLE); + assert.match(err.message, /http 500: x{200}\.\.\./); + assert.equal(err.message.includes('x'.repeat(220)), false); + return true; + }, + ); +}); + +test('AddUser forwards caller-provided 3DES-encrypted password value', async () => { + let captured; + globalThis.fetch = async (url, init) => { + captured = { url, init }; + return { ok: true, status: 200, text: async () => '{"code":0,"data":{"created":true}}' }; + }; + + const result = await rpcdef(buildCtx({ user_name: 'User', login_name: 'user', dept_id: 'dept', password: '3des-ciphertext' }))[ADD_USER](); + + assert.equal(captured.url, 'https://mbs.example/uusafe/mos/thirdaccess/rest/opt/v1/addUser'); + assert.equal(JSON.parse(captured.init.body).password, '3des-ciphertext'); + assert.equal(result.data.structValue.fields.created.boolValue, true); +}); + +test('AddUser omits empty numeric fields instead of sending zero values', async () => { + let captured; + globalThis.fetch = async (url, init) => { + captured = { url, init }; + return { ok: true, status: 200, text: async () => '{"code":0,"data":{}}' }; + }; + + await rpcdef(buildCtx({ user_name: 'User', login_name: 'user', dept_id: 'dept', password: '3des-ciphertext', is_mdm: '', state: '', weight: '' }))[ADD_USER](); + + const body = JSON.parse(captured.init.body); + assert.equal(Object.hasOwn(body, 'isMdm'), false); + assert.equal(Object.hasOwn(body, 'state'), false); + assert.equal(Object.hasOwn(body, 'weight'), false); +}); + +test('UpdUser requires dept_id before calling upstream', async () => { + let called = false; + globalThis.fetch = async () => { + called = true; + return { ok: true, status: 200, text: async () => '{"code":0}' }; + }; + + await assert.rejects( + () => rpcdef(buildCtx({ user_id: 'user-1', user_name: 'User' }))[UPD_USER](), + (err) => { + assert.ok(err instanceof GrpcError); + assert.equal(err.code, grpcStatus.INVALID_ARGUMENT); + assert.equal(err.legacyCode, 'INVALID_ARGUMENT'); + assert.match(err.message, /dept_id required/); + return true; + }, + ); + + assert.equal(called, false); +}); + +test('UpdUser omits empty numeric fields instead of sending zero values', async () => { + let captured; + globalThis.fetch = async (url, init) => { + captured = { url, init }; + return { ok: true, status: 200, text: async () => '{"code":0,"data":{}}' }; + }; + + await rpcdef(buildCtx({ user_id: 'user-1', user_name: 'User', dept_id: 'dept', is_mdm: '', weight: '' }))[UPD_USER](); + + const body = JSON.parse(captured.init.body); + assert.equal(Object.hasOwn(body, 'isMdm'), false); + assert.equal(Object.hasOwn(body, 'weight'), false); +}); + +test('StateUsers requires explicit state before calling upstream', async () => { + let called = false; + globalThis.fetch = async () => { + called = true; + return { ok: true, status: 200, text: async () => '{"code":0}' }; + }; + + await assert.rejects( + () => rpcdef(buildCtx({ type: 0, user_ids: ['user-1'] }))[STATE_USERS](), + (err) => { + assert.ok(err instanceof GrpcError); + assert.equal(err.code, grpcStatus.INVALID_ARGUMENT); + assert.equal(err.legacyCode, 'INVALID_ARGUMENT'); + assert.match(err.message, /state required/); + return true; + }, + ); + + assert.equal(called, false); +}); + +test('DelUsers requires user_ids for type zero before calling upstream', async () => { + let called = false; + globalThis.fetch = async () => { + called = true; + return { ok: true, status: 200, text: async () => '{"code":0}' }; + }; + + await assert.rejects( + () => rpcdef(buildCtx({ type: 0, user_ids: [] }))[DEL_USERS](), + (err) => { + assert.ok(err instanceof GrpcError); + assert.equal(err.code, grpcStatus.INVALID_ARGUMENT); + assert.equal(err.legacyCode, 'INVALID_ARGUMENT'); + assert.match(err.message, /user_ids required for type=0/); + return true; + }, + ); + + assert.equal(called, false); +}); + +test('StateUsers requires user_ids for type zero before calling upstream', async () => { + let called = false; + globalThis.fetch = async () => { + called = true; + return { ok: true, status: 200, text: async () => '{"code":0}' }; + }; + + await assert.rejects( + () => rpcdef(buildCtx({ type: 0, state: 1, user_ids: [] }))[STATE_USERS](), + (err) => { + assert.ok(err instanceof GrpcError); + assert.equal(err.code, grpcStatus.INVALID_ARGUMENT); + assert.equal(err.legacyCode, 'INVALID_ARGUMENT'); + assert.match(err.message, /user_ids required for type=0/); + return true; + }, + ); + + assert.equal(called, false); +}); + +test('DelUsers treats string type zero as userIds mode', async () => { + let captured; + globalThis.fetch = async (url, init) => { + captured = { url, init }; + return { ok: true, status: 200, text: async () => '{"code":0}' }; + }; + + await rpcdef(buildCtx({ type: '0', user_ids: ['user-1'] }))[DEL_USERS](); + + assert.equal(captured.url, 'https://mbs.example/uusafe/mos/thirdaccess/rest/opt/v1/delUsers'); + assert.deepEqual(JSON.parse(captured.init.body).userIds, ['user-1']); + assert.equal(JSON.parse(captured.init.body).type, 0); + assert.equal(Object.hasOwn(JSON.parse(captured.init.body), 'condition'), false); +}); + +test('DelUsers condition mode preserves falsy condition filters', async () => { + let captured; + globalThis.fetch = async (url, init) => { + captured = { url, init }; + return { ok: true, status: 200, text: async () => '{"code":0,"data":null}' }; + }; + + const result = await rpcdef(buildCtx({ type: 1, condition: { key_word: '', status: 0, is_mdm: 0, dept_id: 'dept' } }))[DEL_USERS](); + + assert.equal(captured.url, 'https://mbs.example/uusafe/mos/thirdaccess/rest/opt/v1/delUsers'); + assert.deepEqual(JSON.parse(captured.init.body).condition, { + keyWord: '', + status: 0, + isMdm: 0, + deptId: 'dept', + }); + assert.deepEqual(result.data, { nullValue: 'NULL_VALUE' }); +}); + +test('StateUsers condition mode preserves falsy condition filters', async () => { + let captured; + globalThis.fetch = async (url, init) => { + captured = { url, init }; + return { ok: true, status: 200, text: async () => '{"code":0,"data":{"updated":2}}' }; + }; + + const result = await rpcdef(buildCtx({ type: 1, state: '0', condition: { status: 0, is_mdm: 0 } }))[STATE_USERS](); + + assert.equal(captured.url, 'https://mbs.example/uusafe/mos/thirdaccess/rest/opt/v1/stateUsers'); + assert.equal(JSON.parse(captured.init.body).state, '0'); + assert.deepEqual(JSON.parse(captured.init.body).condition, { + status: 0, + isMdm: 0, + }); + assert.equal(result.data.structValue.fields.updated.numberValue, 2); +}); + +test('UpdUserPwd rejects invalid version before calling upstream', async () => { + let called = false; + globalThis.fetch = async () => { + called = true; + return { ok: true, status: 200, text: async () => '{"code":0}' }; + }; + + await assert.rejects( + () => rpcdef(buildCtx({ version: '../v1', user_id: 'user-1', password: '3des-ciphertext' }))[UPD_USER_PWD](), + (err) => { + assert.ok(err instanceof GrpcError); + assert.equal(err.code, grpcStatus.INVALID_ARGUMENT); + assert.equal(err.legacyCode, 'INVALID_ARGUMENT'); + assert.match(err.message, /version must be v1 or v2/); + return true; + }, + ); + + assert.equal(called, false); +}); + +test('UpdUserPwd v1 requires user_id and password before calling upstream', async () => { + let called = false; + globalThis.fetch = async () => { + called = true; + return { ok: true, status: 200, text: async () => '{"code":0}' }; + }; + + await assert.rejects( + () => rpcdef(buildCtx({ version: 'v1', password: '3des-ciphertext' }))[UPD_USER_PWD](), + (err) => { + assert.ok(err instanceof GrpcError); + assert.equal(err.code, grpcStatus.INVALID_ARGUMENT); + assert.match(err.message, /user_id required for v1/); + return true; + }, + ); + await assert.rejects( + () => rpcdef(buildCtx({ version: 'v1', user_id: 'user-1' }))[UPD_USER_PWD](), + (err) => { + assert.ok(err instanceof GrpcError); + assert.equal(err.code, grpcStatus.INVALID_ARGUMENT); + assert.match(err.message, /password required as 3DES-encrypted value for v1/); + return true; + }, + ); + + assert.equal(called, false); +}); + +test('UpdUserPwd v1 posts to v1 path with encrypted password', async () => { + let captured; + globalThis.fetch = async (url, init) => { + captured = { url, init }; + return { ok: true, status: 200, text: async () => '{"code":0,"data":{}}' }; + }; + + const result = await rpcdef(buildCtx({ version: 'v1', user_id: 'user-1', password: '3des-ciphertext' }))[UPD_USER_PWD](); + + assert.equal(captured.url, 'https://mbs.example/uusafe/mos/thirdaccess/rest/opt/v1/updUserPwd'); + assert.equal(JSON.parse(captured.init.body).userId, 'user-1'); + assert.equal(JSON.parse(captured.init.body).password, '3des-ciphertext'); + assert.deepEqual(result.data, { structValue: { fields: {} } }); +}); + +test('UpdUserPwd v2 requires login_name and new_pwd before calling upstream', async () => { + let called = false; + globalThis.fetch = async () => { + called = true; + return { ok: true, status: 200, text: async () => '{"code":0}' }; + }; + + await assert.rejects( + () => rpcdef(buildCtx({ version: 'v2', new_pwd: 'new-ciphertext' }))[UPD_USER_PWD](), + (err) => { + assert.ok(err instanceof GrpcError); + assert.equal(err.code, grpcStatus.INVALID_ARGUMENT); + assert.match(err.message, /login_name required for v2/); + return true; + }, + ); + await assert.rejects( + () => rpcdef(buildCtx({ version: 'v2', login_name: 'user' }))[UPD_USER_PWD](), + (err) => { + assert.ok(err instanceof GrpcError); + assert.equal(err.code, grpcStatus.INVALID_ARGUMENT); + assert.match(err.message, /new_pwd required as 3DES-encrypted value for v2/); + return true; + }, + ); + + assert.equal(called, false); +}); + +test('UpdUserPwd v2 posts to v2 path and forwards oldPwd', async () => { + let captured; + globalThis.fetch = async (url, init) => { + captured = { url, init }; + return { ok: true, status: 200, text: async () => '{"code":0,"data":{"changed":true}}' }; + }; + + const result = await rpcdef(buildCtx({ version: 'v2', login_name: 'user', old_pwd: 'old-ciphertext', new_pwd: 'new-ciphertext' }))[UPD_USER_PWD](); + + assert.equal(captured.url, 'https://mbs.example/uusafe/mos/thirdaccess/rest/opt/v2/updUserPwd'); + assert.equal(JSON.parse(captured.init.body).loginName, 'user'); + assert.equal(JSON.parse(captured.init.body).oldPwd, 'old-ciphertext'); + assert.equal(JSON.parse(captured.init.body).newPwd, 'new-ciphertext'); + assert.equal(result.data.structValue.fields.changed.boolValue, true); +}); + +test('StateUsers treats string type zero as userIds mode', async () => { + let captured; + globalThis.fetch = async (url, init) => { + captured = { url, init }; + return { ok: true, status: 200, text: async () => '{"code":0}' }; + }; + + await rpcdef(buildCtx({ type: '0', state: '1', user_ids: ['user-1'] }))[STATE_USERS](); + + assert.equal(captured.url, 'https://mbs.example/uusafe/mos/thirdaccess/rest/opt/v1/stateUsers'); + assert.deepEqual(JSON.parse(captured.init.body).userIds, ['user-1']); + assert.equal(JSON.parse(captured.init.body).type, 0); + assert.equal(JSON.parse(captured.init.body).state, '1'); + assert.equal(Object.hasOwn(JSON.parse(captured.init.body), 'condition'), false); +}); diff --git a/services/zhizhangyi__mbs/testdata/test_4apis.mjs b/services/zhizhangyi__mbs/testdata/test_4apis.mjs new file mode 100644 index 00000000..1a7fe25f --- /dev/null +++ b/services/zhizhangyi__mbs/testdata/test_4apis.mjs @@ -0,0 +1,130 @@ +// MBS API 测试: getUsers / detailUser / checkLoginName / stateUsers +import crypto from 'node:crypto'; +import https from 'node:https'; + +const C = { + baseUrl: process.env.MBS_BASE_URL || 'https://127.0.0.1:9074', + orgCode: process.env.MBS_ORG_CODE || '', + appkey: process.env.MBS_APPKEY || '', + secretkey: process.env.MBS_SECRETKEY || '', +}; +const requireConfig = () => { + const missing = Object.entries(C).filter(([, value]) => !value).map(([key]) => key); + if (missing.length > 0) { + throw new Error(`Missing MBS test config: ${missing.join(', ')}. Set MBS_BASE_URL, MBS_ORG_CODE, MBS_APPKEY and MBS_SECRETKEY.`); + } +}; +requireConfig(); +const BASE = `${C.baseUrl}/uusafe/mos/thirdaccess/rest/opt`; +const agent = new https.Agent({ rejectUnauthorized: false }); +const md5 = (s) => crypto.createHash('md5').update(s, 'utf8').digest('hex'); +const sign = (...ps) => md5(ps.map((p) => (p ?? '')).join('') + C.secretkey); + +const post = async (path, body) => { + const res = await fetch(`${BASE}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8' }, body: JSON.stringify(body), agent }); + const text = await res.text(); + return { status: res.status, json: JSON.parse(text) }; +}; + +(async () => { + const { appkey, orgCode } = C; + + // ── Test 1: getUsers ── + console.log('══════════════════════════════════════════'); + console.log('Test 1: getUsers (用户列表)'); + console.log('══════════════════════════════════════════'); + try { + const r1 = await post('/v1/getUsers', { + index: 0, size: 10, orderCode: 0, orderType: 1, + condition: { deptId: '1', keyWord: '' }, + orgCode, appkey, + sign: sign(appkey, orgCode, 0, 10, 0, 1, '', '', '', '1'), + }); + console.log(`HTTP ${r1.status} code=${r1.json.code}`); + if (r1.json.code === 0) { + console.log(` 共 ${r1.json.data.total} 个用户`); + (r1.json.data.userInfos || []).forEach((u, i) => + console.log(` [${i + 1}] ${u.userName} (${u.loginName}) userId=${u.userId} state=${u.state}`)); + } else { console.log(` ❌ ${r1.json.msg}`); } + } catch (e) { console.log(` ❌ ${e.message}`); } + + // ── Test 2: detailUser (test1) ── + console.log('\n══════════════════════════════════════════'); + console.log('Test 2: detailUser (用户详情 - test1)'); + console.log('══════════════════════════════════════════'); + const uid = '1691979294102310912'; // test1 + try { + const r2 = await post('/v1/detailUser', { + userId: uid, orgCode, appkey, + sign: sign(appkey, orgCode, uid), + }); + console.log(`HTTP ${r2.status} code=${r2.json.code}`); + if (r2.json.code === 0) { + const d = r2.json.data; + console.log(` ID: ${d.userId}`); + console.log(` 姓名: ${d.userName}`); + console.log(` 登录名: ${d.loginName}`); + console.log(` 部门: ${d.deptName} (${d.deptId})`); + console.log(` 手机: ${d.phoneNumber || '(空)'}`); + console.log(` 邮箱: ${d.email || '(空)'}`); + console.log(` 状态: ${d.state === 1 ? '启用' : d.state === 0 ? '停用' : '未激活'}`); + console.log(` 设备管理: ${d.isMdm ? '开启' : '关闭'}`); + } else { console.log(` ❌ ${r2.json.msg}`); } + } catch (e) { console.log(` ❌ ${e.message}`); } + + // ── Test 3: checkLoginName ── + console.log('\n══════════════════════════════════════════'); + console.log('Test 3: checkLoginName (账号校验)'); + console.log('══════════════════════════════════════════'); + for (const name of ['test1', 'nonexistent_user']) { + try { + const r3 = await post('/v1/checkLoginName', { + loginName: name, orgCode, appkey, + sign: sign(appkey, orgCode, name), + }); + const ok = r3.json.code === 0; + console.log(` "${name}" -> HTTP ${r3.status} code=${r3.json.code} ${ok ? '✅ 可用' : '❌ ' + r3.json.msg}`); + } catch (e) { console.log(` "${name}" -> ❌ ${e.message}`); } + } + + // ── Test 4: stateUsers (启停 - 先停用 test2 再启用) ── + console.log('\n══════════════════════════════════════════'); + console.log('Test 4: stateUsers (用户启停 - test2)'); + console.log('══════════════════════════════════════════'); + const uid2 = '1691979421122613248'; // test2 + + // 4a: 停用 + console.log(' 4a: 停用 test2...'); + try { + const r4a = await post('/v1/stateUsers', { + userIds: [uid2], type: 0, state: '0', + orgCode, appkey, + sign: sign(appkey, orgCode, uid2, 0, '0', ''), + }); + console.log(` HTTP ${r4a.status} code=${r4a.json.code} ${r4a.json.code === 0 ? '✅ 停用成功' : '❌ ' + r4a.json.msg}`); + } catch (e) { console.log(` ❌ ${e.message}`); } + + // 4b: 验证 - 查详情看 state + try { + const r4b = await post('/v1/detailUser', { + userId: uid2, orgCode, appkey, + sign: sign(appkey, orgCode, uid2), + }); + console.log(` 4b: 验证停用后详情 state=${r4b.json.data?.state} ${r4b.json.data?.state === 0 ? '✅ 已停用' : '⚠️'}`); + } catch (e) { console.log(` 4b: ❌ ${e.message}`); } + + // 4c: 重新启用 + console.log(' 4c: 重新启用 test2...'); + try { + const r4c = await post('/v1/stateUsers', { + userIds: [uid2], type: 0, state: '1', + orgCode, appkey, + sign: sign(appkey, orgCode, uid2, 0, '1', ''), + }); + console.log(` HTTP ${r4c.status} code=${r4c.json.code} ${r4c.json.code === 0 ? '✅ 重新启用成功' : '❌ ' + r4c.json.msg}`); + } catch (e) { console.log(` ❌ ${e.message}`); } + + console.log('\n══════════════════════════════════════════'); + console.log('全部测试完成'); + console.log('══════════════════════════════════════════'); +})(); diff --git a/services/zhizhangyi__mbs/testdata/test_getusers.mjs b/services/zhizhangyi__mbs/testdata/test_getusers.mjs new file mode 100644 index 00000000..a1e4d0ff --- /dev/null +++ b/services/zhizhangyi__mbs/testdata/test_getusers.mjs @@ -0,0 +1,104 @@ +// MBS getUsers API 直连测试 +// 不依赖 OctoBus SDK,直接用 Node.js 原生 fetch 调用 + +import crypto from 'node:crypto'; +import https from 'node:https'; + +const CONFIG = { + baseUrl: process.env.MBS_BASE_URL || 'https://127.0.0.1:9074', + orgCode: process.env.MBS_ORG_CODE || '', + appkey: process.env.MBS_APPKEY || '', + secretkey: process.env.MBS_SECRETKEY || '', +}; + +const md5 = (s) => crypto.createHash('md5').update(s, 'utf8').digest('hex'); + +// sign = MD5(appkey + orgCode + index + size + orderCode + orderType + keyword + state + isMdm + deptId + secretkey) +// 拼接时不包含 "+" +const computeSign = (params) => { + const joined = params.map((p) => (p === undefined || p === null ? '' : String(p))).join(''); + return md5(joined + CONFIG.secretkey); +}; + +const requireConfig = () => { + const missing = Object.entries(CONFIG).filter(([, value]) => !value).map(([key]) => key); + if (missing.length > 0) { + throw new Error(`Missing MBS test config: ${missing.join(', ')}. Set MBS_BASE_URL, MBS_ORG_CODE, MBS_APPKEY and MBS_SECRETKEY.`); + } +}; + +const testGetUsers = async () => { + requireConfig(); + const { baseUrl, orgCode, appkey } = CONFIG; + + const index = 0; + const size = 10; + const orderCode = 0; + const orderType = 1; + const keyword = ''; + const state = ''; + const isMdm = ''; + const deptId = '1'; + + const sign = computeSign([appkey, orgCode, index, size, orderCode, orderType, keyword, state, isMdm, deptId]); + + const body = { + index, + size, + orderCode, + orderType, + condition: { deptId, keyWord: keyword }, + orgCode, + appkey, + sign, + }; + + console.log('=== MBS getUsers 测试 ==='); + console.log('URL:', `${baseUrl}/uusafe/mos/thirdaccess/rest/opt/v1/getUsers`); + console.log('Body:', JSON.stringify(body, null, 2)); + console.log('Sign:', sign); + console.log(''); + + // 忽略自签名证书 + const agent = new https.Agent({ rejectUnauthorized: false }); + + try { + const res = await fetch(`${baseUrl}/uusafe/mos/thirdaccess/rest/opt/v1/getUsers`, { + method: 'POST', + headers: { 'Content-Type': 'application/json; charset=utf-8' }, + body: JSON.stringify(body), + agent, + }); + + const text = await res.text(); + console.log('HTTP Status:', res.status); + console.log('Response headers:', Object.fromEntries(res.headers.entries())); + + let json; + try { + json = JSON.parse(text); + console.log('\n✅ 响应 JSON:'); + console.log(JSON.stringify(json, null, 2)); + + if (json.code === 0) { + console.log(`\n✅ 成功!共 ${json.data?.total ?? 0} 个用户`); + const users = json.data?.userInfos; + if (Array.isArray(users)) { + users.forEach((u, i) => { + console.log(` [${i + 1}] ${u.userName} (${u.loginName}) - ${u.deptName} - ${u.state === 1 ? '启用' : u.state === 0 ? '停用' : '未激活'}`); + }); + } + } else { + console.log(`\n❌ MBS 返回错误: code=${json.code}, msg=${json.msg}`); + } + } catch { + console.log('\n⚠️ 非 JSON 响应:'); + console.log(text.substring(0, 500)); + } + } catch (e) { + console.log('\n❌ 网络错误:', e.message); + if (e.cause) console.log(' 原因:', e.cause.message); + } +}; + +testGetUsers(); diff --git a/services/zhizhangyi__mbs/testdata/test_scenarios.mjs b/services/zhizhangyi__mbs/testdata/test_scenarios.mjs new file mode 100644 index 00000000..e01affb8 --- /dev/null +++ b/services/zhizhangyi__mbs/testdata/test_scenarios.mjs @@ -0,0 +1,101 @@ +// MBS 场景测试: test2停用 / test1设备激活停用 / test1开启设备管控 +import crypto from 'node:crypto'; +import https from 'node:https'; + +const C = { + baseUrl: process.env.MBS_BASE_URL || 'https://127.0.0.1:9074', + orgCode: process.env.MBS_ORG_CODE || '', + appkey: process.env.MBS_APPKEY || '', + secretkey: process.env.MBS_SECRETKEY || '', +}; +const requireConfig = () => { + const missing = Object.entries(C).filter(([, value]) => !value).map(([key]) => key); + if (missing.length > 0) { + throw new Error(`Missing MBS test config: ${missing.join(', ')}. Set MBS_BASE_URL, MBS_ORG_CODE, MBS_APPKEY and MBS_SECRETKEY.`); + } +}; +requireConfig(); +const BASE = `${C.baseUrl}/uusafe/mos/thirdaccess/rest/opt`; +const agent = new https.Agent({ rejectUnauthorized: false }); +const md5 = (s) => crypto.createHash('md5').update(s, 'utf8').digest('hex'); +const sign = (...ps) => md5(ps.map((p) => (p ?? '')).join('') + C.secretkey); + +const post = async (path, body) => { + const res = await fetch(`${BASE}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8' }, body: JSON.stringify(body), agent }); + const text = await res.text(); + return { status: res.status, json: JSON.parse(text) }; +}; + +const { appkey, orgCode } = C; +const uid1 = '1691979294102310912'; // test1 +const uid2 = '1691979421122613248'; // test2 + +// 辅助: 查详情 +const detail = async (uid) => { + const r = await post('/v1/detailUser', { userId: uid, orgCode, appkey, sign: sign(appkey, orgCode, uid) }); + return r.json?.data; +}; + +(async () => { + // 先查初始状态 + console.log('=== 初始状态 ==='); + let d1 = await detail(uid1); + let d2 = await detail(uid2); + console.log(`test1: state=${d1.state} isMdm=${d1.isMdm}`); + console.log(`test2: state=${d2.state} isMdm=${d2.isMdm}`); + + // ═══ 场景1: test2 用户状态 → 停用 (state=0) ═══ + console.log('\n═══ 场景1: test2 用户状态 → 停用 (state=0) ═══'); + const r1 = await post('/v1/stateUsers', { + userIds: [uid2], type: 0, state: '0', + orgCode, appkey, + sign: sign(appkey, orgCode, uid2, 0, '0', ''), + }); + console.log(` stateUsers -> HTTP ${r1.status} code=${r1.json.code} ${r1.json.code === 0 ? '✅' : '❌'}`); + d2 = await detail(uid2); + console.log(` 验证: test2 state=${d2.state} ${d2.state === 0 ? '✅ 已停用' : '⚠️'}`); + + // 恢复 test2 + await post('/v1/stateUsers', { + userIds: [uid2], type: 0, state: '1', + orgCode, appkey, + sign: sign(appkey, orgCode, uid2, 0, '1', ''), + }); + + // ═══ 场景2: test1 设备激活状态 → 停用 (state=0) ═══ + console.log('\n═══ 场景2: test1 设备激活状态 → 停用 (state=0) ═══'); + const r2 = await post('/v1/stateUsers', { + userIds: [uid1], type: 0, state: '0', + orgCode, appkey, + sign: sign(appkey, orgCode, uid1, 0, '0', ''), + }); + console.log(` stateUsers -> HTTP ${r2.status} code=${r2.json.code} ${r2.json.code === 0 ? '✅' : '❌'}`); + d1 = await detail(uid1); + console.log(` 验证: test1 state=${d1.state} ${d1.state === 0 ? '✅ 已停用' : '⚠️'}`); + + // 恢复 test1 + await post('/v1/stateUsers', { + userIds: [uid1], type: 0, state: '1', + orgCode, appkey, + sign: sign(appkey, orgCode, uid1, 0, '1', ''), + }); + + // ═══ 场景3: test1 开启设备管控 (isMdm=1) ═══ + console.log('\n═══ 场景3: test1 开启设备管控 (isMdm=1) ═══'); + console.log(' 调用 updUser 设置 isMdm=1 ...'); + const r3 = await post('/v1/updUser', { + userId: uid1, + userName: '测试1', + loginName: 'test1', + deptId: '1', + isMdm: 1, + orgCode, appkey, + sign: sign(appkey, orgCode, uid1, '测试1', 'test1', '1'), + }); + console.log(` updUser -> HTTP ${r3.status} code=${r3.json.code} ${r3.json.code === 0 ? '✅' : '❌ ' + r3.json.msg}`); + d1 = await detail(uid1); + console.log(` 验证: test1 isMdm=${d1.isMdm} ${d1.isMdm === 1 ? '✅ 设备管控已开启' : '⚠️'}`); + console.log(` test1 state=${d1.state} (应仍为1-启用)`); + + console.log('\n=== 全部场景测试完成 ==='); +})();