@oj-module/cli 0.2.46 → 0.2.48
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/devkit/SKILL.md +1 -1
- package/devkit/framework-dev.md +2 -2
- package/devkit/manual.md +24 -14
- package/devkit/module-dev-and-cdn-playground.md +2 -2
- package/devkit/wecom-daikaifa.md +282 -27
- package/package.json +2 -2
- package/shell-dist/assets/runtime.js +2 -2
- package/shell-dist/index.html +2 -2
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/templates-tenant-scope.test.ts +7 -2
- package/src/init.ts +1 -1
- package/src/market/__tests__/bootstrap.test.ts +1 -1
- package/src/market/__tests__/setup.test.ts +7 -7
- package/src/market/bootstrap.ts +1 -1
- package/templates/README.md +1 -1
- package/templates/config.yaml +26 -7
- package/templates/init.include.yaml +12 -0
- package/templates/openapi.json +2863 -549
- package/templates/src/_platform/api/_shared/service/domain/contacts.ts +7 -0
- package/templates/src/_platform/api/_shared/service/domain/org-read.ts +451 -0
- package/templates/src/_platform/api/_shared/service/domain/org.ts +566 -0
- package/templates/src/_platform/api/_shared/service/domain/users.ts +140 -2
- package/templates/src/_platform/api/_shared/service/infra/query.ts +2 -1
- package/templates/src/_platform/api/fixtures/l1-test-user.sql +1 -1
- package/templates/src/_platform/api/manifest.yaml +3 -0
- package/templates/src/_platform/api/migrations/0016__org.mysql.sql +41 -0
- package/templates/src/_platform/api/migrations/0016__org.pg.sql +40 -0
- package/templates/src/_platform/api/migrations/0016__org.sql +62 -0
- package/templates/src/_platform/api/org/departments/api.ts +51 -0
- package/templates/src/_platform/api/org/is-member/api.ts +33 -0
- package/templates/src/_platform/api/org/members/api.ts +55 -0
- package/templates/src/_platform/api/org/picker/api.ts +59 -0
- package/templates/src/_platform/api/org/resolve/api.ts +38 -0
- package/templates/src/_platform/api/org/tree/api.ts +37 -0
- package/templates/src/_platform/api/org/user/api.ts +46 -0
- package/templates/src/_platform/api/schema.yaml +48 -0
- package/templates/src/_platform/api/seed.mysql.sql +10 -5
- package/templates/src/_platform/api/seed.pg.sql +10 -5
- package/templates/src/_platform/api/seed.sql +13 -5
- package/templates/src/_platform/web/__client/api.schemas.ts +126 -0
- package/templates/src/_platform/web/__client/api.ts +170 -1
- package/templates/src/auth/api/_shared/ldap-login.ts +2 -0
- package/templates/src/auth/api/_shared/session.ts +5 -1
- package/templates/src/auth/api/_shared/wecom/admin.ts +17 -10
- package/templates/src/auth/api/_shared/wecom/client.ts +311 -40
- package/templates/src/auth/api/_shared/wecom/config.ts +40 -17
- package/templates/src/auth/api/_shared/wecom/contacts.ts +263 -0
- package/templates/src/auth/api/_shared/wecom/crypt.ts +17 -7
- package/templates/src/auth/api/_shared/wecom/events.ts +111 -26
- package/templates/src/auth/api/_shared/wecom/login.ts +152 -1
- package/templates/src/auth/api/_shared/wecom/payload.ts +116 -0
- package/templates/src/auth/api/_shared/wecom/store.ts +65 -0
- package/templates/src/auth/api/_shared/wecom/sync.ts +487 -0
- package/templates/src/auth/api/manifest.yaml +3 -0
- package/templates/src/auth/api/migrations/0002__org_sync.mysql.sql +28 -0
- package/templates/src/auth/api/migrations/0002__org_sync.pg.sql +27 -0
- package/templates/src/auth/api/migrations/0002__org_sync.sql +41 -0
- package/templates/src/auth/api/schema.yaml +31 -0
- package/templates/src/auth/api/wecom/admin/api.ts +7 -5
- package/templates/src/auth/api/wecom/api.ts +100 -18
- package/templates/src/auth/api/wecom/sync/api.ts +199 -0
- package/templates/src/auth/web/__client/api.schemas.ts +97 -0
- package/templates/src/auth/web/__client/api.ts +73 -1
- package/templates/src/login/web/auth-provider.ts +9 -1
- package/templates/src/notification/api/seed.sql +1 -1
- package/templates/src/system/api/departments/api.ts +142 -0
- package/templates/src/system/api/departments/detach/api.ts +34 -0
- package/templates/src/system/api/departments/members/api.ts +142 -0
- package/templates/src/system/api/departments/move/api.ts +39 -0
- package/templates/src/system/api/users/api.ts +45 -5
- package/templates/src/system/api/users/profile/api.ts +44 -0
- package/templates/src/system/web/__client/api.schemas.ts +143 -0
- package/templates/src/system/web/__client/api.ts +293 -1
- package/templates/src/system/web/entry.ts +6 -0
- package/templates/src/system/web/locales/en-US.json +113 -7
- package/templates/src/system/web/locales/zh-CN.json +113 -7
- package/templates/src/system/web/pages/departments/index.tsx +346 -0
- package/templates/src/system/web/pages/departments/sync-bar.tsx +257 -0
- package/templates/src/system/web/pages/departments/tree.ts +66 -0
- package/templates/src/system/web/pages/errors.ts +1 -1
- package/templates/src/system/web/pages/users/index.tsx +49 -7
- package/templates/src/system/web/pages/wecom/index.tsx +16 -1
- package/templates/tests/l1-builder.test.ts +1 -1
- package/templates/tests/org-departments.spec.ts +311 -0
- package/templates/tests/org-read.spec.ts +469 -0
- package/templates/tests/rbac-contacts.spec.ts +13 -3
- package/templates/tests/rbac-seed.spec.ts +11 -0
- package/templates/tests/seed-role-accounts.spec.ts +6 -2
- package/templates/tests/wecom-admin.spec.ts +64 -42
- package/templates/tests/wecom-client-timeout.spec.ts +99 -0
- package/templates/tests/wecom-connector.spec.ts +115 -11
- package/templates/tests/wecom-crypt.spec.ts +12 -3
- package/templates/tests/wecom-endpoints.spec.ts +182 -14
- package/templates/tests/wecom-org-sync.spec.ts +598 -0
- package/templates/tests/wecom-sync.spec.ts +286 -0
- package/vendor/host-versions.json +3 -3
- package/templates/src/auth/api/_shared/wecom/xml.ts +0 -36
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { respond } from "../../_shared/respond";
|
|
2
|
+
// GET /api/_platform/org/user?uid= —— 某人的部门路径 / 主部门 / 职务(共享读面)。
|
|
3
|
+
// 不给 uid 即查自己;scope=self 下问别人 → 403(不是 404:不泄露账号存在性)。
|
|
4
|
+
import { readUserDepartments, resolveReadFace } from "../../_shared/service/domain/org-read";
|
|
5
|
+
|
|
6
|
+
async function get() {
|
|
7
|
+
await respond(async () => {
|
|
8
|
+
const q = http.query as Record<string, unknown>;
|
|
9
|
+
const reader = await resolveReadFace(q.scope);
|
|
10
|
+
return readUserDepartments(reader, q.uid == null || String(q.uid).trim() === "" ? reader.uid : Number(q.uid));
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
get.schema = {
|
|
14
|
+
query: {
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: {
|
|
17
|
+
scope: { type: "string", enum: ["self", "all"] },
|
|
18
|
+
uid: { type: "number" },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
get.response = {
|
|
23
|
+
type: "object",
|
|
24
|
+
required: ["userId", "username", "position", "mainDeptId", "mainDeptPath", "departments"],
|
|
25
|
+
properties: {
|
|
26
|
+
userId: { type: "number" },
|
|
27
|
+
username: { type: "string" },
|
|
28
|
+
displayName: { type: ["string", "null"] },
|
|
29
|
+
position: { type: ["string", "null"] },
|
|
30
|
+
mainDeptId: { type: ["number", "null"] },
|
|
31
|
+
mainDeptPath: { type: "array", items: { type: "string" } },
|
|
32
|
+
departments: {
|
|
33
|
+
type: "array",
|
|
34
|
+
items: {
|
|
35
|
+
type: "object",
|
|
36
|
+
required: ["deptId", "deptPath"],
|
|
37
|
+
properties: {
|
|
38
|
+
deptId: { type: "number" },
|
|
39
|
+
deptPath: { type: "array", items: { type: "string" } },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default { get };
|
|
@@ -32,6 +32,14 @@ tables:
|
|
|
32
32
|
email: {type: text}
|
|
33
33
|
phone_number: {type: text}
|
|
34
34
|
description: {type: text}
|
|
35
|
+
# 0016:是否登录过本站。同步会为「从未登录」的全员建档,contacts 只返回 activated=1,
|
|
36
|
+
# 否则任何登录账号都能以 60 次/分把整份企业名册拖走(方案 §9.1)。存量行迁移时置 1。
|
|
37
|
+
activated: {type: integer, null: false}
|
|
38
|
+
# 职务(与来源无关的通用字段)
|
|
39
|
+
position: {type: text}
|
|
40
|
+
# 外部源侧的状态快照(企微 1/2/4/5)。**绝不**自动改写 is_forbidden——那会和
|
|
41
|
+
# 本站管理员的手动启停互相覆盖(方案 §4.3)
|
|
42
|
+
external_status: {type: integer}
|
|
35
43
|
sys_setting:
|
|
36
44
|
# 共享表(B 组 D8):登录方式发现 /system/login-methods 是未登录端点,此刻没有租户上下文;
|
|
37
45
|
# 且这些开关是部署级语义,不是每 corp 语义。
|
|
@@ -163,3 +171,43 @@ tables:
|
|
|
163
171
|
source_url: {type: text, null: false}
|
|
164
172
|
max_seq: {type: integer, null: false}
|
|
165
173
|
updated_at: {type: integer, null: false}
|
|
174
|
+
# ---- 0016:组织架构领域(CRUD 基线 + 外部源适配器的落点)----
|
|
175
|
+
# 定位(方案 §14):CRUD 是基线,企微 / LDAP 只是适配器 → 表里不出现任何 wecom_* / ldap_* 字段。
|
|
176
|
+
# origin 是**归属权**标识(D13):local = 本站管理员全权;wecom/ldap/sso = 该适配器管辖,
|
|
177
|
+
# 同步与回收只作用于自己 origin 的行,绝不碰别的(否则本地建的部门会被同步干掉)。
|
|
178
|
+
sys_department:
|
|
179
|
+
# 租户表:组织架构天然按租户隔离(企微部署下 tenant_id = corp_id),
|
|
180
|
+
# 只在有租户上下文时被读写,与 users / sys_oplog 同组(0015 A 组口径)。
|
|
181
|
+
pk: id
|
|
182
|
+
columns:
|
|
183
|
+
id: {type: integer, autoincrement: true}
|
|
184
|
+
tenant_id: {type: text, null: false}
|
|
185
|
+
origin: {type: text, null: false}
|
|
186
|
+
# 外部源主键(企微部门 id / LDAP entryUUID);origin='local' 时由应用层生成稳定串
|
|
187
|
+
origin_id: {type: text, null: false}
|
|
188
|
+
# 本地父部门 id(不是外部 id):本地部门与同步部门可混挂,且不随外部调整而变
|
|
189
|
+
parent_id: {type: integer}
|
|
190
|
+
# 未获权限时为 NULL(企微代开发未授权「部门名」即如此),展示层用 origin_id 兜底
|
|
191
|
+
name: {type: text}
|
|
192
|
+
name_en: {type: text}
|
|
193
|
+
order_no: {type: integer, null: false}
|
|
194
|
+
# 同步可见性,**仅由同步轮次维护**(失去可见范围 → 0);本地部门恒 1
|
|
195
|
+
sync_visible: {type: integer, null: false}
|
|
196
|
+
# 软删除。只由外部源的删除事件置 1(D9):本地删除走真删
|
|
197
|
+
deleted: {type: integer, null: false}
|
|
198
|
+
# 最近一次完整全量命中的 job(墓碑回收前必先校验本轮完整且无 skip)
|
|
199
|
+
seen_job_id: {type: integer}
|
|
200
|
+
update_time: {type: integer, null: false}
|
|
201
|
+
# 成员 ↔ 部门(多对多):企微一人可在多个部门,order_no / is_leader 与 department[] 逐位对应。
|
|
202
|
+
# 全本地 id 关联,**与来源完全无关**——接任何适配器都不用动这张表。
|
|
203
|
+
sys_dept_member:
|
|
204
|
+
pk: id
|
|
205
|
+
columns:
|
|
206
|
+
id: {type: integer, autoincrement: true}
|
|
207
|
+
tenant_id: {type: text, null: false}
|
|
208
|
+
user_id: {type: integer, null: false}
|
|
209
|
+
dept_id: {type: integer, null: false}
|
|
210
|
+
order_no: {type: integer, null: false}
|
|
211
|
+
is_leader: {type: integer, null: false}
|
|
212
|
+
is_main: {type: integer, null: false}
|
|
213
|
+
update_time: {type: integer, null: false}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
-- _platform 种子数据 mysql 方言覆盖(方言覆盖文件必胜;INSERT OR IGNORE → INSERT IGNORE)。
|
|
2
|
-
-- 语句分号切分与注释纪律同基线 seed.sql
|
|
3
|
-
INSERT IGNORE INTO users (username, password_hash, must_change) VALUES ('superadmin', '$2y$10$
|
|
4
|
-
INSERT IGNORE INTO users (username, password_hash, must_change) VALUES ('admin', '$2y$10$
|
|
5
|
-
INSERT IGNORE INTO users (username, password_hash, must_change) VALUES ('editor', '$2y$10$
|
|
6
|
-
INSERT IGNORE INTO users (username, password_hash, must_change) VALUES ('viewer', '$2y$10$
|
|
2
|
+
-- 语句分号切分与注释纪律同基线 seed.sql;账号口径同基线(账号名=角色名,口令 aA123456,must_change=0)。
|
|
3
|
+
INSERT IGNORE INTO users (username, password_hash, must_change) VALUES ('superadmin', '$2y$10$9HEtASi9jG0VIBMMVqxAmOqrE4f6wiezhGmi50iPG1s1MXlncSO0e', 0);
|
|
4
|
+
INSERT IGNORE INTO users (username, password_hash, must_change) VALUES ('admin', '$2y$10$9HEtASi9jG0VIBMMVqxAmOqrE4f6wiezhGmi50iPG1s1MXlncSO0e', 0);
|
|
5
|
+
INSERT IGNORE INTO users (username, password_hash, must_change) VALUES ('editor', '$2y$10$9HEtASi9jG0VIBMMVqxAmOqrE4f6wiezhGmi50iPG1s1MXlncSO0e', 0);
|
|
6
|
+
INSERT IGNORE INTO users (username, password_hash, must_change) VALUES ('viewer', '$2y$10$9HEtASi9jG0VIBMMVqxAmOqrE4f6wiezhGmi50iPG1s1MXlncSO0e', 0);
|
|
7
7
|
DELETE FROM sys_user_role WHERE role_name = 'superadmin' AND user_id = (SELECT id FROM users WHERE username = 'admin');
|
|
8
8
|
UPDATE users SET must_change = 0 WHERE username IN ('superadmin', 'admin', 'editor', 'viewer');
|
|
9
9
|
INSERT IGNORE INTO sys_role (name, module, title, builtin) VALUES ('superadmin', 'framework', '超级管理员', 1);
|
|
@@ -14,3 +14,8 @@ INSERT IGNORE INTO sys_user_role (user_id, role_name) SELECT id, 'superadmin' FR
|
|
|
14
14
|
INSERT IGNORE INTO sys_user_role (user_id, role_name) SELECT id, 'admin' FROM users WHERE username = 'admin';
|
|
15
15
|
INSERT IGNORE INTO sys_user_role (user_id, role_name) SELECT id, 'editor' FROM users WHERE username = 'editor';
|
|
16
16
|
INSERT IGNORE INTO sys_user_role (user_id, role_name) SELECT id, 'viewer' FROM users WHERE username = 'viewer';
|
|
17
|
+
-- 组织架构共享读面全量权限点(基线 seed 同款,注释不重复):scope=all 挂 platform.org.read.all
|
|
18
|
+
INSERT IGNORE INTO sys_permission (code, module, title_key, deprecated) VALUES ('platform.org.read.all', 'framework', 'system:perm.platform_org_read_all', 0);
|
|
19
|
+
-- 企微组织架构同步(B2):触发同步是打上游的动作,挂权限点;admin/superadmin 天然通过,
|
|
20
|
+
-- 只有自定义角色需要显式绑定(与 platform.org.read.all 同一条纪律:不落库就永远绑不上)。
|
|
21
|
+
INSERT IGNORE INTO sys_permission (code, module, title_key, deprecated) VALUES ('auth.wecom.sync', 'auth', 'system:perm.auth_wecom_sync', 0);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
-- _platform 种子数据 pg 方言覆盖(方言覆盖文件必胜;INSERT OR IGNORE → ON CONFLICT DO NOTHING)。
|
|
2
|
-
-- 语句分号切分与注释纪律同基线 seed.sql
|
|
3
|
-
INSERT INTO users (username, password_hash, must_change) VALUES ('superadmin', '$2y$10$
|
|
4
|
-
INSERT INTO users (username, password_hash, must_change) VALUES ('admin', '$2y$10$
|
|
5
|
-
INSERT INTO users (username, password_hash, must_change) VALUES ('editor', '$2y$10$
|
|
6
|
-
INSERT INTO users (username, password_hash, must_change) VALUES ('viewer', '$2y$10$
|
|
2
|
+
-- 语句分号切分与注释纪律同基线 seed.sql;账号口径同基线(账号名=角色名,口令 aA123456,must_change=0)。
|
|
3
|
+
INSERT INTO users (username, password_hash, must_change) VALUES ('superadmin', '$2y$10$9HEtASi9jG0VIBMMVqxAmOqrE4f6wiezhGmi50iPG1s1MXlncSO0e', 0) ON CONFLICT DO NOTHING;
|
|
4
|
+
INSERT INTO users (username, password_hash, must_change) VALUES ('admin', '$2y$10$9HEtASi9jG0VIBMMVqxAmOqrE4f6wiezhGmi50iPG1s1MXlncSO0e', 0) ON CONFLICT DO NOTHING;
|
|
5
|
+
INSERT INTO users (username, password_hash, must_change) VALUES ('editor', '$2y$10$9HEtASi9jG0VIBMMVqxAmOqrE4f6wiezhGmi50iPG1s1MXlncSO0e', 0) ON CONFLICT DO NOTHING;
|
|
6
|
+
INSERT INTO users (username, password_hash, must_change) VALUES ('viewer', '$2y$10$9HEtASi9jG0VIBMMVqxAmOqrE4f6wiezhGmi50iPG1s1MXlncSO0e', 0) ON CONFLICT DO NOTHING;
|
|
7
7
|
DELETE FROM sys_user_role WHERE role_name = 'superadmin' AND user_id = (SELECT id FROM users WHERE username = 'admin');
|
|
8
8
|
UPDATE users SET must_change = 0 WHERE username IN ('superadmin', 'admin', 'editor', 'viewer');
|
|
9
9
|
INSERT INTO sys_role (name, module, title, builtin) VALUES ('superadmin', 'framework', '超级管理员', 1) ON CONFLICT DO NOTHING;
|
|
@@ -14,3 +14,8 @@ INSERT INTO sys_user_role (user_id, role_name) SELECT id, 'superadmin' FROM user
|
|
|
14
14
|
INSERT INTO sys_user_role (user_id, role_name) SELECT id, 'admin' FROM users WHERE username = 'admin' ON CONFLICT DO NOTHING;
|
|
15
15
|
INSERT INTO sys_user_role (user_id, role_name) SELECT id, 'editor' FROM users WHERE username = 'editor' ON CONFLICT DO NOTHING;
|
|
16
16
|
INSERT INTO sys_user_role (user_id, role_name) SELECT id, 'viewer' FROM users WHERE username = 'viewer' ON CONFLICT DO NOTHING;
|
|
17
|
+
-- 组织架构共享读面全量权限点(基线 seed 同款,注释不重复):scope=all 挂 platform.org.read.all
|
|
18
|
+
INSERT INTO sys_permission (code, module, title_key, deprecated) VALUES ('platform.org.read.all', 'framework', 'system:perm.platform_org_read_all', 0) ON CONFLICT DO NOTHING;
|
|
19
|
+
-- 企微组织架构同步(B2):触发同步是打上游的动作,挂权限点;admin/superadmin 天然通过,
|
|
20
|
+
-- 只有自定义角色需要显式绑定(与 platform.org.read.all 同一条纪律:不落库就永远绑不上)。
|
|
21
|
+
INSERT INTO sys_permission (code, module, title_key, deprecated) VALUES ('auth.wecom.sync', 'auth', 'system:perm.auth_wecom_sync', 0) ON CONFLICT DO NOTHING;
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
-- 方言覆盖:seed.mysql.sql / seed.pg.sql(INSERT OR IGNORE 为 sqlite 专属语法,方言文件必胜,同迁移约定)。
|
|
3
3
|
-- 注意:seed 按英文分号朴素切分语句,注释里也不要出现英文分号。
|
|
4
4
|
-- 账号生命周期 D12(2026-10-22 调整):四个内置角色各一个开发账号,**账号名=角色名**
|
|
5
|
-
-- (superadmin/admin/editor/viewer,避免心智负担),口令均
|
|
5
|
+
-- (superadmin/admin/editor/viewer,避免心智负担),口令均 aA123456(bcrypt cost=10、$2y$ 前缀,
|
|
6
|
+
-- 2026-11-01 由 123456 改来),must_change=0 免首登改密——
|
|
6
7
|
-- 纯开发便利(默认凭据不随机生成的取舍见计划 A2 阶段小结),生产部署须改密或删除
|
|
7
|
-
INSERT OR IGNORE INTO users (username, password_hash, must_change) VALUES ('superadmin', '$2y$10$
|
|
8
|
-
INSERT OR IGNORE INTO users (username, password_hash, must_change) VALUES ('admin', '$2y$10$
|
|
9
|
-
INSERT OR IGNORE INTO users (username, password_hash, must_change) VALUES ('editor', '$2y$10$
|
|
10
|
-
INSERT OR IGNORE INTO users (username, password_hash, must_change) VALUES ('viewer', '$2y$10$
|
|
8
|
+
INSERT OR IGNORE INTO users (username, password_hash, must_change) VALUES ('superadmin', '$2y$10$9HEtASi9jG0VIBMMVqxAmOqrE4f6wiezhGmi50iPG1s1MXlncSO0e', 0);
|
|
9
|
+
INSERT OR IGNORE INTO users (username, password_hash, must_change) VALUES ('admin', '$2y$10$9HEtASi9jG0VIBMMVqxAmOqrE4f6wiezhGmi50iPG1s1MXlncSO0e', 0);
|
|
10
|
+
INSERT OR IGNORE INTO users (username, password_hash, must_change) VALUES ('editor', '$2y$10$9HEtASi9jG0VIBMMVqxAmOqrE4f6wiezhGmi50iPG1s1MXlncSO0e', 0);
|
|
11
|
+
INSERT OR IGNORE INTO users (username, password_hash, must_change) VALUES ('viewer', '$2y$10$9HEtASi9jG0VIBMMVqxAmOqrE4f6wiezhGmi50iPG1s1MXlncSO0e', 0);
|
|
11
12
|
-- 历史 seed 把 admin 绑在 superadmin 上;收敛为账号名=角色名(幂等 DELETE,每次重放生效)。
|
|
12
13
|
-- 已存在库中 admin 行可能残留 must_change=1,UPDATE 统一收敛为 0(四个均为 seed 自有开发账号)
|
|
13
14
|
DELETE FROM sys_user_role WHERE role_name = 'superadmin' AND user_id = (SELECT id FROM users WHERE username = 'admin');
|
|
@@ -22,6 +23,10 @@ INSERT OR IGNORE INTO sys_user_role (user_id, role_name) SELECT id, 'superadmin'
|
|
|
22
23
|
INSERT OR IGNORE INTO sys_user_role (user_id, role_name) SELECT id, 'admin' FROM users WHERE username = 'admin';
|
|
23
24
|
INSERT OR IGNORE INTO sys_user_role (user_id, role_name) SELECT id, 'editor' FROM users WHERE username = 'editor';
|
|
24
25
|
INSERT OR IGNORE INTO sys_user_role (user_id, role_name) SELECT id, 'viewer' FROM users WHERE username = 'viewer';
|
|
26
|
+
-- 组织架构共享读面的全量权限点(方案 D5):scope=all 挂它,没挂的人只能看自己的部门路径。
|
|
27
|
+
-- ⚡ 权限点**必须先落库**才能被管理员绑给 custom 角色(createRole/updateBindings 会校验 code 存在),
|
|
28
|
+
-- 只在端点里 assertPermission 而不落库 = 这个权限点永远绑不上,门禁形同虚设。
|
|
29
|
+
INSERT OR IGNORE INTO sys_permission (code, module, title_key, deprecated) VALUES ('platform.org.read.all', 'framework', 'system:perm.platform_org_read_all', 0);
|
|
25
30
|
-- 系统设置默认值(PRD §4.3;C4 设置页 / C5 登录方式发现的数据源;framework.market_cdn_url 安全组仅 superadmin 可改)
|
|
26
31
|
-- framework.market_cdn_url 缺省 /cdn = 本站点自供(config.yaml 的 /cdn 挂载直出工程根 cdn/),脚手架免 setup;外部真 CDN 才改指绝对地址
|
|
27
32
|
INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('framework.site_title', 'framework', 'general', 'string', 'React Antd Admin', 'React Antd Admin', 'system:setting.site_title', 0, 0);
|
|
@@ -34,3 +39,6 @@ INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_va
|
|
|
34
39
|
INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('ldap_base_dn', 'ldap', 'ldap', 'string', '', '', 'system:setting.ldap_base_dn', 0, 0);
|
|
35
40
|
INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('ldap_uid_attr', 'ldap', 'ldap', 'string', 'uid', 'uid', 'system:setting.ldap_uid_attr', 0, 0);
|
|
36
41
|
INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('ldap_email_attr', 'ldap', 'ldap', 'string', 'mail', 'mail', 'system:setting.ldap_email_attr', 0, 0);
|
|
42
|
+
-- 企微组织架构同步(B2):触发同步是打上游的动作,挂权限点;admin/superadmin 天然通过,
|
|
43
|
+
-- 只有自定义角色需要显式绑定(与 platform.org.read.all 同一条纪律:不落库就永远绑不上)。
|
|
44
|
+
INSERT OR IGNORE INTO sys_permission (code, module, title_key, deprecated) VALUES ('auth.wecom.sync', 'auth', 'system:perm.auth_wecom_sync', 0);
|
|
@@ -102,4 +102,130 @@ export const schemas = {
|
|
|
102
102
|
|
|
103
103
|
}).catchall(z.unknown())),
|
|
104
104
|
},
|
|
105
|
+
"platformOrgTreeGet": {
|
|
106
|
+
query: z.object({
|
|
107
|
+
"scope": z.enum(["self", "all"]).optional()
|
|
108
|
+
}),
|
|
109
|
+
data: z.array(z.object({
|
|
110
|
+
"id": z.number(),
|
|
111
|
+
"parentId": z.number().nullable(),
|
|
112
|
+
"name": z.string().nullable(),
|
|
113
|
+
"nameEn": z.string().nullable().optional(),
|
|
114
|
+
"orderNo": z.number(),
|
|
115
|
+
"memberCount": z.number()
|
|
116
|
+
})),
|
|
117
|
+
},
|
|
118
|
+
"platformOrgDepartmentsGet": {
|
|
119
|
+
query: z.object({
|
|
120
|
+
"scope": z.enum(["self", "all"]).optional(),
|
|
121
|
+
"parent": z.number().optional(),
|
|
122
|
+
"keyword": z.string().optional(),
|
|
123
|
+
"page": z.number().min(1).optional(),
|
|
124
|
+
"size": z.number().min(1).max(200).optional()
|
|
125
|
+
}),
|
|
126
|
+
data: z.object({
|
|
127
|
+
"list": z.array(z.object({
|
|
128
|
+
"id": z.number(),
|
|
129
|
+
"parentId": z.number().nullable(),
|
|
130
|
+
"name": z.string().nullable(),
|
|
131
|
+
"nameEn": z.string().nullable().optional(),
|
|
132
|
+
"orderNo": z.number(),
|
|
133
|
+
"memberCount": z.number()
|
|
134
|
+
})),
|
|
135
|
+
"total": z.number(),
|
|
136
|
+
"page": z.number(),
|
|
137
|
+
"size": z.number(),
|
|
138
|
+
"truncated": z.boolean()
|
|
139
|
+
}),
|
|
140
|
+
},
|
|
141
|
+
"platformOrgMembersGet": {
|
|
142
|
+
query: z.object({
|
|
143
|
+
"scope": z.enum(["self", "all"]).optional(),
|
|
144
|
+
"dept": z.number().optional(),
|
|
145
|
+
"includeSub": z.string().optional(),
|
|
146
|
+
"keyword": z.string().optional(),
|
|
147
|
+
"page": z.number().min(1).optional(),
|
|
148
|
+
"size": z.number().min(1).max(200).optional()
|
|
149
|
+
}),
|
|
150
|
+
data: z.object({
|
|
151
|
+
"list": z.array(z.object({
|
|
152
|
+
"userId": z.number(),
|
|
153
|
+
"username": z.string(),
|
|
154
|
+
"displayName": z.string().nullable().optional(),
|
|
155
|
+
"deptId": z.number(),
|
|
156
|
+
"deptPath": z.array(z.string()),
|
|
157
|
+
"isMain": z.boolean()
|
|
158
|
+
})),
|
|
159
|
+
"total": z.number(),
|
|
160
|
+
"page": z.number(),
|
|
161
|
+
"size": z.number(),
|
|
162
|
+
"truncated": z.boolean()
|
|
163
|
+
}),
|
|
164
|
+
},
|
|
165
|
+
"platformOrgUserGet": {
|
|
166
|
+
query: z.object({
|
|
167
|
+
"scope": z.enum(["self", "all"]).optional(),
|
|
168
|
+
"uid": z.number().optional()
|
|
169
|
+
}),
|
|
170
|
+
data: z.object({
|
|
171
|
+
"userId": z.number(),
|
|
172
|
+
"username": z.string(),
|
|
173
|
+
"displayName": z.string().nullable().optional(),
|
|
174
|
+
"position": z.string().nullable(),
|
|
175
|
+
"mainDeptId": z.number().nullable(),
|
|
176
|
+
"mainDeptPath": z.array(z.string()),
|
|
177
|
+
"departments": z.array(z.object({
|
|
178
|
+
"deptId": z.number(),
|
|
179
|
+
"deptPath": z.array(z.string())
|
|
180
|
+
}))
|
|
181
|
+
}),
|
|
182
|
+
},
|
|
183
|
+
"platformOrgPickerGet": {
|
|
184
|
+
query: z.object({
|
|
185
|
+
"scope": z.enum(["self", "all"]).optional(),
|
|
186
|
+
"keyword": z.string().min(2),
|
|
187
|
+
"limit": z.number().min(1).max(50).optional()
|
|
188
|
+
}),
|
|
189
|
+
data: z.object({
|
|
190
|
+
"departments": z.array(z.object({
|
|
191
|
+
"id": z.number(),
|
|
192
|
+
"parentId": z.number().nullable(),
|
|
193
|
+
"name": z.string().nullable(),
|
|
194
|
+
"nameEn": z.string().nullable().optional(),
|
|
195
|
+
"orderNo": z.number(),
|
|
196
|
+
"memberCount": z.number()
|
|
197
|
+
})),
|
|
198
|
+
"users": z.array(z.object({
|
|
199
|
+
"userId": z.number(),
|
|
200
|
+
"username": z.string(),
|
|
201
|
+
"displayName": z.string().nullable().optional(),
|
|
202
|
+
"deptId": z.number(),
|
|
203
|
+
"deptPath": z.array(z.string()),
|
|
204
|
+
"isMain": z.boolean()
|
|
205
|
+
}))
|
|
206
|
+
}),
|
|
207
|
+
},
|
|
208
|
+
"platformOrgResolvePost": {
|
|
209
|
+
query: z.object({
|
|
210
|
+
"scope": z.enum(["self", "all"]).optional()
|
|
211
|
+
}),
|
|
212
|
+
body: z.object({
|
|
213
|
+
"deptIds": z.array(z.number()).optional(),
|
|
214
|
+
"userIds": z.array(z.number()).optional(),
|
|
215
|
+
"includeSub": z.boolean().optional()
|
|
216
|
+
}),
|
|
217
|
+
data: z.object({
|
|
218
|
+
"userIds": z.array(z.number())
|
|
219
|
+
}),
|
|
220
|
+
},
|
|
221
|
+
"platformOrgIsMemberGet": {
|
|
222
|
+
query: z.object({
|
|
223
|
+
"scope": z.enum(["self", "all"]).optional(),
|
|
224
|
+
"uid": z.number().optional(),
|
|
225
|
+
"dept": z.number()
|
|
226
|
+
}),
|
|
227
|
+
data: z.object({
|
|
228
|
+
"isMember": z.boolean()
|
|
229
|
+
}),
|
|
230
|
+
},
|
|
105
231
|
};
|
|
@@ -33,7 +33,7 @@ export function create_platformClient(ctx: ModuleContext) {
|
|
|
33
33
|
}
|
|
34
34
|
ctx.register.apiPrefix(API_PREFIX);
|
|
35
35
|
bindRequest(ctx.utils.request);
|
|
36
|
-
return { platformContactsGet, platformContactsDetailGet, platformMarketInstallPost, platformMarketInstallCancelPost, platformMarketTaskGet, platformMarketEnablePost, platformMarketMetaGet, platformMarketTrustGet, platformMarketTrustPost, platformMarketTrustKeyPost, platformMarketTrustKeyDelete, platformMarketIndexGet, platformMarketUninstallImpactGet, platformMarketUninstallPost, platformMarketHistoryGet, platformSolutionsRoutesGet, platformSolutionsVerifyPost, platformSolutionsVerifyLastGet };
|
|
36
|
+
return { platformContactsGet, platformContactsDetailGet, platformMarketInstallPost, platformMarketInstallCancelPost, platformMarketTaskGet, platformMarketEnablePost, platformMarketMetaGet, platformMarketTrustGet, platformMarketTrustPost, platformMarketTrustKeyPost, platformMarketTrustKeyDelete, platformMarketIndexGet, platformMarketUninstallImpactGet, platformMarketUninstallPost, platformMarketHistoryGet, platformSolutionsRoutesGet, platformSolutionsVerifyPost, platformSolutionsVerifyLastGet, platformOrgTreeGet, platformOrgDepartmentsGet, platformOrgMembersGet, platformOrgUserGet, platformOrgPickerGet, platformOrgResolvePost, platformOrgIsMemberGet };
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
@@ -446,3 +446,172 @@ export async function platformSolutionsVerifyLastGet(): Promise<unknown> {
|
|
|
446
446
|
throw await toApiError(e);
|
|
447
447
|
}
|
|
448
448
|
}
|
|
449
|
+
|
|
450
|
+
export type PlatformOrgTreeGetQuery = z.input<(typeof schemas)["platformOrgTreeGet"]["query"]>;
|
|
451
|
+
export type PlatformOrgTreeGetData = z.infer<(typeof schemas)["platformOrgTreeGet"]["data"]>;
|
|
452
|
+
|
|
453
|
+
export async function platformOrgTreeGet(query: PlatformOrgTreeGetQuery): Promise<PlatformOrgTreeGetData> {
|
|
454
|
+
const client = ensureReq();
|
|
455
|
+
try {
|
|
456
|
+
const env = await client.get(`_platform/org/tree`, { searchParams: query as Record<string, string | number | boolean> }).json<OjEnvelope<PlatformOrgTreeGetData>>();
|
|
457
|
+
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
458
|
+
if (typeof env.code === "number" && env.code !== 0)
|
|
459
|
+
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
460
|
+
const data = env.data as PlatformOrgTreeGetData;
|
|
461
|
+
if (import.meta.env.DEV) {
|
|
462
|
+
const { schemas } = await import("./api.schemas");
|
|
463
|
+
const r = schemas["platformOrgTreeGet"].data.safeParse(data);
|
|
464
|
+
if (!r.success)
|
|
465
|
+
throw new ContractApiError(-1, `[契约违例] platformOrgTreeGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
|
|
466
|
+
}
|
|
467
|
+
return data;
|
|
468
|
+
}
|
|
469
|
+
catch (e) {
|
|
470
|
+
throw await toApiError(e);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export type PlatformOrgDepartmentsGetQuery = z.input<(typeof schemas)["platformOrgDepartmentsGet"]["query"]>;
|
|
475
|
+
export type PlatformOrgDepartmentsGetData = z.infer<(typeof schemas)["platformOrgDepartmentsGet"]["data"]>;
|
|
476
|
+
|
|
477
|
+
export async function platformOrgDepartmentsGet(query: PlatformOrgDepartmentsGetQuery): Promise<PlatformOrgDepartmentsGetData> {
|
|
478
|
+
const client = ensureReq();
|
|
479
|
+
try {
|
|
480
|
+
const env = await client.get(`_platform/org/departments`, { searchParams: query as Record<string, string | number | boolean> }).json<OjEnvelope<PlatformOrgDepartmentsGetData>>();
|
|
481
|
+
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
482
|
+
if (typeof env.code === "number" && env.code !== 0)
|
|
483
|
+
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
484
|
+
const data = env.data as PlatformOrgDepartmentsGetData;
|
|
485
|
+
if (import.meta.env.DEV) {
|
|
486
|
+
const { schemas } = await import("./api.schemas");
|
|
487
|
+
const r = schemas["platformOrgDepartmentsGet"].data.safeParse(data);
|
|
488
|
+
if (!r.success)
|
|
489
|
+
throw new ContractApiError(-1, `[契约违例] platformOrgDepartmentsGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
|
|
490
|
+
}
|
|
491
|
+
return data;
|
|
492
|
+
}
|
|
493
|
+
catch (e) {
|
|
494
|
+
throw await toApiError(e);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export type PlatformOrgMembersGetQuery = z.input<(typeof schemas)["platformOrgMembersGet"]["query"]>;
|
|
499
|
+
export type PlatformOrgMembersGetData = z.infer<(typeof schemas)["platformOrgMembersGet"]["data"]>;
|
|
500
|
+
|
|
501
|
+
export async function platformOrgMembersGet(query: PlatformOrgMembersGetQuery): Promise<PlatformOrgMembersGetData> {
|
|
502
|
+
const client = ensureReq();
|
|
503
|
+
try {
|
|
504
|
+
const env = await client.get(`_platform/org/members`, { searchParams: query as Record<string, string | number | boolean> }).json<OjEnvelope<PlatformOrgMembersGetData>>();
|
|
505
|
+
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
506
|
+
if (typeof env.code === "number" && env.code !== 0)
|
|
507
|
+
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
508
|
+
const data = env.data as PlatformOrgMembersGetData;
|
|
509
|
+
if (import.meta.env.DEV) {
|
|
510
|
+
const { schemas } = await import("./api.schemas");
|
|
511
|
+
const r = schemas["platformOrgMembersGet"].data.safeParse(data);
|
|
512
|
+
if (!r.success)
|
|
513
|
+
throw new ContractApiError(-1, `[契约违例] platformOrgMembersGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
|
|
514
|
+
}
|
|
515
|
+
return data;
|
|
516
|
+
}
|
|
517
|
+
catch (e) {
|
|
518
|
+
throw await toApiError(e);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export type PlatformOrgUserGetQuery = z.input<(typeof schemas)["platformOrgUserGet"]["query"]>;
|
|
523
|
+
export type PlatformOrgUserGetData = z.infer<(typeof schemas)["platformOrgUserGet"]["data"]>;
|
|
524
|
+
|
|
525
|
+
export async function platformOrgUserGet(query: PlatformOrgUserGetQuery): Promise<PlatformOrgUserGetData> {
|
|
526
|
+
const client = ensureReq();
|
|
527
|
+
try {
|
|
528
|
+
const env = await client.get(`_platform/org/user`, { searchParams: query as Record<string, string | number | boolean> }).json<OjEnvelope<PlatformOrgUserGetData>>();
|
|
529
|
+
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
530
|
+
if (typeof env.code === "number" && env.code !== 0)
|
|
531
|
+
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
532
|
+
const data = env.data as PlatformOrgUserGetData;
|
|
533
|
+
if (import.meta.env.DEV) {
|
|
534
|
+
const { schemas } = await import("./api.schemas");
|
|
535
|
+
const r = schemas["platformOrgUserGet"].data.safeParse(data);
|
|
536
|
+
if (!r.success)
|
|
537
|
+
throw new ContractApiError(-1, `[契约违例] platformOrgUserGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
|
|
538
|
+
}
|
|
539
|
+
return data;
|
|
540
|
+
}
|
|
541
|
+
catch (e) {
|
|
542
|
+
throw await toApiError(e);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export type PlatformOrgPickerGetQuery = z.input<(typeof schemas)["platformOrgPickerGet"]["query"]>;
|
|
547
|
+
export type PlatformOrgPickerGetData = z.infer<(typeof schemas)["platformOrgPickerGet"]["data"]>;
|
|
548
|
+
|
|
549
|
+
export async function platformOrgPickerGet(query: PlatformOrgPickerGetQuery): Promise<PlatformOrgPickerGetData> {
|
|
550
|
+
const client = ensureReq();
|
|
551
|
+
try {
|
|
552
|
+
const env = await client.get(`_platform/org/picker`, { searchParams: query as Record<string, string | number | boolean> }).json<OjEnvelope<PlatformOrgPickerGetData>>();
|
|
553
|
+
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
554
|
+
if (typeof env.code === "number" && env.code !== 0)
|
|
555
|
+
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
556
|
+
const data = env.data as PlatformOrgPickerGetData;
|
|
557
|
+
if (import.meta.env.DEV) {
|
|
558
|
+
const { schemas } = await import("./api.schemas");
|
|
559
|
+
const r = schemas["platformOrgPickerGet"].data.safeParse(data);
|
|
560
|
+
if (!r.success)
|
|
561
|
+
throw new ContractApiError(-1, `[契约违例] platformOrgPickerGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
|
|
562
|
+
}
|
|
563
|
+
return data;
|
|
564
|
+
}
|
|
565
|
+
catch (e) {
|
|
566
|
+
throw await toApiError(e);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export type PlatformOrgResolvePostQuery = z.input<(typeof schemas)["platformOrgResolvePost"]["query"]>;
|
|
571
|
+
export type PlatformOrgResolvePostBody = z.input<(typeof schemas)["platformOrgResolvePost"]["body"]>;
|
|
572
|
+
export type PlatformOrgResolvePostData = z.infer<(typeof schemas)["platformOrgResolvePost"]["data"]>;
|
|
573
|
+
|
|
574
|
+
export async function platformOrgResolvePost(input: { query: PlatformOrgResolvePostQuery, body: PlatformOrgResolvePostBody }): Promise<PlatformOrgResolvePostData> {
|
|
575
|
+
const client = ensureReq();
|
|
576
|
+
try {
|
|
577
|
+
const env = await client.post(`_platform/org/resolve`, { searchParams: input.query as Record<string, string | number | boolean>, json: input.body }).json<OjEnvelope<PlatformOrgResolvePostData>>();
|
|
578
|
+
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
579
|
+
if (typeof env.code === "number" && env.code !== 0)
|
|
580
|
+
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
581
|
+
const data = env.data as PlatformOrgResolvePostData;
|
|
582
|
+
if (import.meta.env.DEV) {
|
|
583
|
+
const { schemas } = await import("./api.schemas");
|
|
584
|
+
const r = schemas["platformOrgResolvePost"].data.safeParse(data);
|
|
585
|
+
if (!r.success)
|
|
586
|
+
throw new ContractApiError(-1, `[契约违例] platformOrgResolvePost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
|
|
587
|
+
}
|
|
588
|
+
return data;
|
|
589
|
+
}
|
|
590
|
+
catch (e) {
|
|
591
|
+
throw await toApiError(e);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export type PlatformOrgIsMemberGetQuery = z.input<(typeof schemas)["platformOrgIsMemberGet"]["query"]>;
|
|
596
|
+
export type PlatformOrgIsMemberGetData = z.infer<(typeof schemas)["platformOrgIsMemberGet"]["data"]>;
|
|
597
|
+
|
|
598
|
+
export async function platformOrgIsMemberGet(query: PlatformOrgIsMemberGetQuery): Promise<PlatformOrgIsMemberGetData> {
|
|
599
|
+
const client = ensureReq();
|
|
600
|
+
try {
|
|
601
|
+
const env = await client.get(`_platform/org/is-member`, { searchParams: query as Record<string, string | number | boolean> }).json<OjEnvelope<PlatformOrgIsMemberGetData>>();
|
|
602
|
+
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
603
|
+
if (typeof env.code === "number" && env.code !== 0)
|
|
604
|
+
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
605
|
+
const data = env.data as PlatformOrgIsMemberGetData;
|
|
606
|
+
if (import.meta.env.DEV) {
|
|
607
|
+
const { schemas } = await import("./api.schemas");
|
|
608
|
+
const r = schemas["platformOrgIsMemberGet"].data.safeParse(data);
|
|
609
|
+
if (!r.success)
|
|
610
|
+
throw new ContractApiError(-1, `[契约违例] platformOrgIsMemberGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
|
|
611
|
+
}
|
|
612
|
+
return data;
|
|
613
|
+
}
|
|
614
|
+
catch (e) {
|
|
615
|
+
throw await toApiError(e);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
@@ -74,6 +74,8 @@ export function createLdapLogin(session: Pick<Session, "issueExternal">, gateway
|
|
|
74
74
|
email: firstAttr(entries[0].attrs, emailAttr) || null,
|
|
75
75
|
must_change: 0,
|
|
76
76
|
is_forbidden: 0,
|
|
77
|
+
// 能走到这里说明 LDAP bind 成功 = 真人本人在登录,进通讯录目录
|
|
78
|
+
activated: 1,
|
|
77
79
|
}).run();
|
|
78
80
|
}
|
|
79
81
|
catch (e) {
|
|
@@ -100,12 +100,16 @@ export function createSession(deps: SessionDeps = runtimeDeps()) {
|
|
|
100
100
|
return {
|
|
101
101
|
/** B1+D14:本地登录——先验口令后查禁用;号不存在/口令错/禁用统一 401(不泄露账号状态)。 */
|
|
102
102
|
async localLogin(username: string, password: string): Promise<TokenBundle> {
|
|
103
|
-
const rows = await db.table("users").select(["id", "password_hash", "is_forbidden"]).where({ field: "username", op: "eq", value: String(username ?? "") }).all() as Array<Record<string, unknown>>;
|
|
103
|
+
const rows = await db.table("users").select(["id", "password_hash", "is_forbidden", "activated"]).where({ field: "username", op: "eq", value: String(username ?? "") }).all() as Array<Record<string, unknown>>;
|
|
104
104
|
const row = rows[0];
|
|
105
105
|
if (!row || !(await deps.bcryptVerify(String(password ?? ""), String(row.password_hash))))
|
|
106
106
|
fail(401, "invalid credentials");
|
|
107
107
|
if (Number(row.is_forbidden) === 1)
|
|
108
108
|
fail(401, "invalid credentials");
|
|
109
|
+
// 首次成功登录 = 这个号确实有真人在用 → 放行到通讯录目录(同步建的全员档初始 activated=0)。
|
|
110
|
+
// 只在 0→1 时写一次,老用户登录不产生额外 UPDATE。
|
|
111
|
+
if (Number(row.activated) === 0)
|
|
112
|
+
await db.table("users").update({ activated: 1 }).where({ field: "id", op: "eq", value: Number(row.id) }).run();
|
|
109
113
|
return issueFor(Number(row.id));
|
|
110
114
|
},
|
|
111
115
|
|
|
@@ -12,8 +12,8 @@ import type { WecomClient } from "./client";
|
|
|
12
12
|
import type { WecomConfig } from "./config";
|
|
13
13
|
import type { WecomStoreDeps } from "./store";
|
|
14
14
|
import { invalidateAllUsers } from "#/_platform/_shared/service/infra/access";
|
|
15
|
-
import {
|
|
16
|
-
import { corpScope,
|
|
15
|
+
import { assertProviderConfig, eventUrlOf } from "./config";
|
|
16
|
+
import { corpScope, revokeTenantSessions, tenantOfCorp } from "./store";
|
|
17
17
|
|
|
18
18
|
export interface WecomAdminDeps {
|
|
19
19
|
client: WecomClient
|
|
@@ -29,13 +29,15 @@ export interface AppRow {
|
|
|
29
29
|
env: string
|
|
30
30
|
corp_name: string | null
|
|
31
31
|
status: string
|
|
32
|
+
/** 0=管理员授权 / 1=成员授权:成员授权下这家企业的应用做不了组织架构同步(§2.3) */
|
|
33
|
+
auth_mode: number
|
|
32
34
|
installed_at: number
|
|
33
35
|
update_time: number
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export async function listApps(): Promise<AppRow[]> {
|
|
37
39
|
const rows = await db.table("wecom_app_profile")
|
|
38
|
-
.select(["suite_id", "corp_id", "agent_id", "auth_corp_id", "env", "corp_name", "status", "installed_at", "update_time"])
|
|
40
|
+
.select(["suite_id", "corp_id", "agent_id", "auth_corp_id", "env", "corp_name", "status", "auth_mode", "installed_at", "update_time"])
|
|
39
41
|
.all() as unknown as AppRow[];
|
|
40
42
|
// 稳定序:先按企业、再按应用;同企业多环境并排,运维一眼能看出「哪两个是同一家」
|
|
41
43
|
return rows.sort((a, b) => a.corp_id.localeCompare(b.corp_id) || a.agent_id.localeCompare(b.agent_id));
|
|
@@ -88,25 +90,30 @@ export async function setAppStatus(
|
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
export interface InstallResult {
|
|
93
|
+
/** 开通链接(代开发 = `get_customized_auth_url` 返回的 qrcode_url,企业管理员扫/开即授权) */
|
|
91
94
|
installUrl: string
|
|
92
95
|
/** 企微后台「指令回调 URL」应填这个(逐字) */
|
|
93
96
|
callbackUrl: string
|
|
97
|
+
/** 链接有效期(秒,以企微返回为准,可达 10 天) */
|
|
94
98
|
expiresIn: number
|
|
95
99
|
}
|
|
96
100
|
|
|
97
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* 生成开通入口(代开发):provider_access_token → get_customized_auth_url → qrcode_url。
|
|
103
|
+
* 与第三方应用的「预授权码拼安装链接」不是一个接口——代开发**不支持** get_pre_auth_code
|
|
104
|
+
* (errcode 48002),必须用这个。授权完成由企微回调 create_auth 落库,与这里无关。
|
|
105
|
+
*/
|
|
98
106
|
export async function createInstall(
|
|
99
107
|
cfg: WecomConfig,
|
|
100
108
|
client: WecomClient,
|
|
101
|
-
store: WecomStoreDeps,
|
|
102
109
|
state: string,
|
|
103
110
|
): Promise<InstallResult> {
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
const
|
|
111
|
+
assertProviderConfig(cfg);
|
|
112
|
+
const providerToken = await client.providerAccessToken(cfg.providerId, cfg.providerSecret);
|
|
113
|
+
const auth = await client.customizedAuthUrl(providerToken, [cfg.suiteId], state);
|
|
107
114
|
return {
|
|
108
|
-
installUrl:
|
|
115
|
+
installUrl: auth.url,
|
|
109
116
|
callbackUrl: eventUrlOf(cfg),
|
|
110
|
-
expiresIn:
|
|
117
|
+
expiresIn: auth.expiresIn,
|
|
111
118
|
};
|
|
112
119
|
}
|