@oj-module/cli 0.2.5 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/devkit/manual.md +19 -17
- package/devkit/sql-builder-missing-capabilities-pr.md +234 -0
- 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/contract/__tests__/handler-scan.test.ts +29 -0
- package/src/contract/__tests__/market-client.integration.test.ts +167 -0
- package/src/contract/handler-scan.ts +35 -4
- package/src/dev.ts +10 -2
- package/src/index.ts +3 -3
- package/src/init.ts +44 -0
- package/src/preview-config.ts +163 -0
- package/src/preview.ts +105 -56
- package/src/usage.ts +3 -2
- package/templates/api/config.yaml +4 -2
- package/templates/api/src/_platform/README.md +31 -0
- package/templates/api/src/_platform/_lifecycle/atomic.ts +1 -1
- package/templates/api/src/_platform/_lifecycle/history.ts +10 -5
- package/templates/api/src/_platform/_lifecycle/installer.ts +4 -4
- package/templates/api/src/_platform/_lifecycle/sha384.ts +1 -1
- package/templates/api/src/_platform/_lifecycle/steps/download.ts +1 -1
- package/templates/api/src/_platform/_lifecycle/steps/register.ts +57 -29
- package/templates/api/src/_platform/_lifecycle/trust.ts +1 -1
- package/templates/api/src/_platform/_lifecycle/uninstall.ts +13 -8
- package/templates/api/src/_platform/_lifecycle/verify-checks.ts +2 -2
- package/templates/api/src/_platform/_shared/service/{contacts.ts → domain/contacts.ts} +8 -6
- package/templates/api/src/_platform/_shared/service/{roles.ts → domain/roles.ts} +18 -19
- package/templates/api/src/_platform/_shared/service/{settings.ts → domain/settings.ts} +6 -6
- package/templates/api/src/_platform/_shared/service/{solution-perms.ts → domain/solution-perms.ts} +18 -17
- package/templates/api/src/_platform/_shared/service/{users.ts → domain/users.ts} +20 -26
- package/templates/api/src/_platform/_shared/service/{access.ts → infra/access.ts} +5 -8
- package/templates/api/src/_platform/_shared/service/{audit.ts → infra/audit.ts} +26 -26
- package/templates/api/src/_platform/_shared/service/{rate.ts → infra/rate.ts} +12 -12
- package/templates/api/src/_platform/_shared/site-facade.ts +1 -1
- package/templates/api/src/_platform/contacts/api.ts +2 -2
- package/templates/api/src/_platform/contacts/detail/api.ts +2 -2
- package/templates/api/src/_platform/manifest.yaml +3 -1
- package/templates/api/src/_platform/market/api.ts +54 -16
- package/templates/api/src/_platform/schema.yaml +14 -0
- package/templates/api/src/_platform/solutions/api.ts +13 -12
- package/templates/api/src/auth/_shared/ldap-login.ts +11 -7
- package/templates/api/src/auth/_shared/login-throttle.ts +2 -2
- package/templates/api/src/auth/_shared/session.ts +25 -21
- package/templates/api/src/auth/api.ts +1 -1
- package/templates/api/src/demo/todos/api.ts +1 -0
- package/templates/api/src/home/line/api.ts +4 -4
- package/templates/api/src/home/pie/api.ts +4 -4
- package/templates/api/src/notification/_shared/notify.ts +40 -38
- package/templates/api/src/notifications/api.ts +1 -4
- package/templates/api/src/personal-center/upload/api.ts +1 -1
- package/templates/api/src/system/login-methods/api.ts +1 -1
- package/templates/api/src/system/oplog/api.ts +3 -3
- package/templates/api/src/system/permissions/api.ts +2 -2
- package/templates/api/src/system/roles/api.ts +2 -2
- package/templates/api/src/system/roles/bindings/api.ts +2 -2
- package/templates/api/src/system/settings/api.ts +2 -2
- package/templates/api/src/system/users/api.ts +2 -2
- package/templates/api/src/system/users/forbidden/api.ts +2 -2
- package/templates/api/src/system/users/password/api.ts +2 -2
- package/templates/api/src/system/users/roles/api.ts +2 -2
- package/templates/api/src/web/user-info/api.ts +2 -5
- package/templates/api/tests/auth-session.spec.ts +1 -1
- package/templates/api/tests/auth-throttle.spec.ts +1 -1
- package/templates/api/tests/guard.spec.ts +1 -1
- package/templates/api/tests/lifecycle/market-api.spec.ts +1 -1
- package/templates/api/tests/lifecycle/trust.spec.ts +1 -1
- package/templates/api/tests/lifecycle/uninstall.spec.ts +1 -1
- package/templates/api/tests/query-utils.spec.ts +1 -1
- package/templates/api/tests/rbac-audit.spec.ts +1 -1
- package/templates/api/tests/rbac-contacts.spec.ts +2 -2
- package/templates/api/tests/rbac-guard.spec.ts +1 -1
- package/templates/api/tests/rbac-roles.spec.ts +2 -2
- package/templates/api/tests/rbac-settings.spec.ts +2 -2
- package/templates/api/tests/rbac-solution-perms.spec.ts +1 -1
- package/templates/api/tests/rbac-users.spec.ts +1 -1
- package/templates/api/tsconfig.json +8 -0
- package/templates/env.d.ts +2 -0
- package/templates/global.d.ts +816 -0
- package/templates/init.include.yaml +18 -1
- package/templates/logs/task-events.jsonl +0 -0
- package/templates/openapi.json +525 -13
- package/templates/package.json +2 -1
- package/templates/web/src/_platform/__client/api.schemas.ts +97 -0
- package/templates/web/src/_platform/__client/api.ts +152 -39
- package/templates/web/src/oj-market/__tests__/CardGrid.test.tsx +19 -0
- package/templates/web/src/oj-market/__tests__/InstallDialog.test.tsx +3 -0
- package/templates/web/src/oj-market/__tests__/RollbackDialog.test.tsx +1 -0
- package/templates/web/src/oj-market/__tests__/marketClient.fake.ts +1 -0
- package/templates/web/src/oj-market/api/client.ts +5 -105
- package/templates/web/src/oj-market/api/defaultClient.ts +111 -5
- package/templates/web/src/oj-market/components/CardGrid.tsx +4 -1
- package/templates/web/src/oj-market/components/CardItem.tsx +9 -5
- package/templates/web/src/oj-market/components/DetailDrawer.tsx +7 -3
- package/templates/web/src/oj-market/components/InstallDialog.tsx +1 -1
- package/templates/web/src/oj-market/hooks/useMarket.ts +4 -1
- package/templates/web/src/oj-market/locales/en-US.json +2 -1
- package/templates/web/src/oj-market/locales/zh-CN.json +2 -1
- package/templates/web/src/oj-market/pages/MarketApp.tsx +16 -1
- package/templates/web.config.ts +3 -3
- package/vendor/host-versions.json +3 -3
- /package/templates/api/src/_platform/_shared/service/{guard.ts → infra/guard.ts} +0 -0
- /package/templates/api/src/_platform/_shared/service/{oplog.ts → infra/oplog.ts} +0 -0
- /package/templates/api/src/_platform/_shared/service/{query.ts → infra/query.ts} +0 -0
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
// 是其既有语义,本文件不越界代改)。DB 级路由占用检查承接 P1-5 备案③(pre-DB 阶段无真源)。
|
|
9
9
|
// db 为 isolate 全局(同模板 service 层,无需 import;测试由夹具装 globalThis.db)。
|
|
10
10
|
|
|
11
|
-
import type { SolutionManifest } from "../../_shared/service/solution-perms";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
11
|
+
import type { SolutionManifest } from "../../_shared/service/domain/solution-perms";
|
|
12
|
+
import { upsertSolutionPerms } from "../../_shared/service/domain/solution-perms";
|
|
13
|
+
import { auditOp } from "../../_shared/service/infra/audit";
|
|
14
14
|
|
|
15
15
|
export interface RouteDecl {
|
|
16
16
|
path: string
|
|
@@ -76,19 +76,19 @@ function firstSeg(path: string): string {
|
|
|
76
76
|
* 让位须走卸载),自占排除(module ≠ 自己)。
|
|
77
77
|
*/
|
|
78
78
|
export async function checkOccupancy(module: string, routePaths: string[]): Promise<string[]> {
|
|
79
|
-
const rows = await db.
|
|
79
|
+
const rows = await db.table("sys_solution_route").select(["path"]).where({ field: "module", op: "ne", value: module }).all() as Array<{ path: string }>;
|
|
80
80
|
const taken = new Set(rows.map(r => firstSeg(String(r.path))));
|
|
81
81
|
return routePaths.filter(p => taken.has(firstSeg(p)));
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
/** 声明消失 → 行保留标 deprecated=1(镜像 solution-perms AC2-3/4),返回被标 key/path 清单 */
|
|
85
85
|
async function deprecateGone(table: "sys_setting" | "sys_solution_route", col: "key" | "path", module: string, declared: Set<string>): Promise<string[]> {
|
|
86
|
-
const old = await db.
|
|
86
|
+
const old = await db.table(table).select([col]).where({ field: "module", op: "eq", value: module }).where({ field: "deprecated", op: "eq", value: 0 }).all() as Array<Record<string, unknown>>;
|
|
87
87
|
const gone: string[] = [];
|
|
88
88
|
for (const r of old) {
|
|
89
89
|
const k = String(r[col]);
|
|
90
90
|
if (!declared.has(k)) {
|
|
91
|
-
await db.
|
|
91
|
+
await db.table(table).update({ deprecated: 1 }).where({ field: col, op: "eq", value: k }).run();
|
|
92
92
|
gone.push(k);
|
|
93
93
|
}
|
|
94
94
|
}
|
|
@@ -122,36 +122,64 @@ export async function registerSolution(decl: SolutionDecl): Promise<RegisterResu
|
|
|
122
122
|
// 设置项 upsert:存在只覆定义列(value/update 语义 = 管理员改过的值不复活,AC1-15),
|
|
123
123
|
// 不存在插 default_value(首装 value)
|
|
124
124
|
for (const s of settings) {
|
|
125
|
-
const exists = await db.
|
|
125
|
+
const exists = await db.table("sys_setting").select(["key"]).where({ field: "key", op: "eq", value: s.key }).all() as unknown[];
|
|
126
126
|
if (exists.length) {
|
|
127
|
-
await db.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
127
|
+
await db.table("sys_setting").update({
|
|
128
|
+
module: decl.module,
|
|
129
|
+
group: s.group,
|
|
130
|
+
type: s.type,
|
|
131
|
+
default_value: s.default_value ?? null,
|
|
132
|
+
label_key: s.label_key ?? null,
|
|
133
|
+
group_label_key: s.group_label_key ?? null,
|
|
134
|
+
options: s.options ?? null,
|
|
135
|
+
secret: s.secret ? 1 : 0,
|
|
136
|
+
deprecated: 0,
|
|
137
|
+
update_time: Date.now(),
|
|
138
|
+
}).where({ field: "key", op: "eq", value: s.key }).run();
|
|
131
139
|
}
|
|
132
140
|
else {
|
|
133
|
-
await db.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
141
|
+
await db.table("sys_setting").insert({
|
|
142
|
+
key: s.key,
|
|
143
|
+
module: decl.module,
|
|
144
|
+
group: s.group,
|
|
145
|
+
type: s.type,
|
|
146
|
+
value: s.value ?? s.default_value ?? null,
|
|
147
|
+
default_value: s.default_value ?? null,
|
|
148
|
+
label_key: s.label_key ?? null,
|
|
149
|
+
group_label_key: s.group_label_key ?? null,
|
|
150
|
+
options: s.options ?? null,
|
|
151
|
+
secret: s.secret ? 1 : 0,
|
|
152
|
+
update_time: Date.now(),
|
|
153
|
+
deprecated: 0,
|
|
154
|
+
}).run();
|
|
137
155
|
}
|
|
138
156
|
}
|
|
139
157
|
const settingDeps = await deprecateGone("sys_setting", "key", decl.module, new Set(settings.map(s => s.key)));
|
|
140
158
|
|
|
141
159
|
// 路由逐行 upsert:同 path 覆元数据;声明行复位 deprecated=0(重声明回到快照)
|
|
142
160
|
for (const r of routes) {
|
|
143
|
-
const exists = await db.
|
|
161
|
+
const exists = await db.table("sys_solution_route").select(["path"]).where({ field: "path", op: "eq", value: r.path }).all() as unknown[];
|
|
144
162
|
if (exists.length) {
|
|
145
|
-
await db.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
163
|
+
await db.table("sys_solution_route").update({
|
|
164
|
+
module: decl.module,
|
|
165
|
+
category: r.category,
|
|
166
|
+
order: r.order,
|
|
167
|
+
title_key: r.title_key ?? null,
|
|
168
|
+
required_permission: r.required_permission ?? null,
|
|
169
|
+
deprecated: 0,
|
|
170
|
+
}).where({ field: "path", op: "eq", value: r.path }).run();
|
|
149
171
|
}
|
|
150
172
|
else {
|
|
151
|
-
await db.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
173
|
+
await db.table("sys_solution_route").insert({
|
|
174
|
+
path: r.path,
|
|
175
|
+
module: decl.module,
|
|
176
|
+
category: r.category,
|
|
177
|
+
order: r.order,
|
|
178
|
+
title_key: r.title_key ?? null,
|
|
179
|
+
required_permission: r.required_permission ?? null,
|
|
180
|
+
deprecated: 0,
|
|
181
|
+
enabled: 1,
|
|
182
|
+
}).run();
|
|
155
183
|
}
|
|
156
184
|
}
|
|
157
185
|
const routeDeps = await deprecateGone("sys_solution_route", "path", decl.module, new Set(routes.map(r => r.path)));
|
|
@@ -177,10 +205,10 @@ export async function registerSolution(decl: SolutionDecl): Promise<RegisterResu
|
|
|
177
205
|
}
|
|
178
206
|
|
|
179
207
|
export async function queryRegistered(name: string): Promise<SolutionSnapshot | null> {
|
|
180
|
-
const routes = (await db.
|
|
181
|
-
const settings = (await db.
|
|
182
|
-
const roles = (await db.
|
|
183
|
-
const permissions = (await db.
|
|
208
|
+
const routes = (await db.table("sys_solution_route").select(["path"]).where({ field: "module", op: "eq", value: name }).where({ field: "deprecated", op: "eq", value: 0 }).all() as Array<{ path: string }>).map(r => String(r.path)).sort();
|
|
209
|
+
const settings = (await db.table("sys_setting").select(["key"]).where({ field: "module", op: "eq", value: name }).where({ field: "deprecated", op: "eq", value: 0 }).all() as Array<{ key: string }>).map(r => String(r.key)).sort();
|
|
210
|
+
const roles = (await db.table("sys_role").select(["name"]).where({ field: "module", op: "eq", value: name }).where({ field: "deprecated", op: "eq", value: 0 }).all() as Array<{ name: string }>).map(r => String(r.name)).sort();
|
|
211
|
+
const permissions = (await db.table("sys_permission").select(["code"]).where({ field: "module", op: "eq", value: name }).where({ field: "deprecated", op: "eq", value: 0 }).all() as Array<{ code: string }>).map(r => String(r.code)).sort();
|
|
184
212
|
if (routes.length + settings.length + roles.length + permissions.length === 0) {
|
|
185
213
|
return null;
|
|
186
214
|
}
|
|
@@ -195,7 +223,7 @@ export async function queryAllRegistered(): Promise<SolutionSnapshot[]> {
|
|
|
195
223
|
const names = new Set<string>();
|
|
196
224
|
for (const table of ["sys_solution_route", "sys_setting", "sys_role", "sys_permission"]) {
|
|
197
225
|
// 表名为字面常量,无拼接注入面
|
|
198
|
-
const rows = await db.
|
|
226
|
+
const rows = await db.table(table).distinct().select(["module"]).where({ field: "deprecated", op: "eq", value: 0 }).all() as Array<{ module: string }>;
|
|
199
227
|
for (const r of rows) {
|
|
200
228
|
names.add(String(r.module));
|
|
201
229
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { auditOp } from "../_shared/service/audit";
|
|
1
|
+
import { auditOp } from "../_shared/service/infra/audit";
|
|
2
2
|
// 宿主信任库(P2-2,契约 v1.3 §2.11 / C2-38/39,G3 双轨第一轨 / G5)。
|
|
3
3
|
// 存储:.solutions/trusted-keys.yaml(key_id → {public_key, added_at})——YAML 受限子集
|
|
4
4
|
// (两层 map + 标量,同 state.ts 口径:只读回本模块写出的内容,不引 yaml 依赖,严格解析)。
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
// 任一步失败返回 {ok:false, message}(与 setEnabled 同纪律);部分残差由 admin 重试或重装自愈。
|
|
8
8
|
// SQL 全部参数化(与 audit.ts / setEnabled 同纪律,值不拼字符串)。
|
|
9
9
|
|
|
10
|
-
import type { OpEntry } from "../_shared/service/audit";
|
|
10
|
+
import type { OpEntry } from "../_shared/service/infra/audit";
|
|
11
11
|
import type { StateStore } from "./state";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
12
|
+
import { uninstallSolution } from "../_shared/service/domain/solution-perms";
|
|
13
|
+
import { invalidateAllUsers } from "../_shared/service/infra/access";
|
|
14
|
+
import { auditOp } from "../_shared/service/infra/audit";
|
|
15
15
|
import { atomicWriteText, fsFace } from "./atomic";
|
|
16
16
|
import { recordHistory } from "./history";
|
|
17
17
|
import { readLock, writeLock } from "./lock";
|
|
@@ -20,10 +20,12 @@ import { readLock, writeLock } from "./lock";
|
|
|
20
20
|
declare const db: {
|
|
21
21
|
exec: (sql: string, params: unknown[]) => Promise<unknown>
|
|
22
22
|
query: (sql: string, params?: unknown[]) => Promise<unknown>
|
|
23
|
+
/** 安全查询构造器(迁移目标;与 query 同返回行数组) */
|
|
24
|
+
table: (name: string) => any
|
|
23
25
|
/** 当前方言(sqlite/mysql/pg);缺省 sqlite。Q4 业务表目录查询方言无关 */
|
|
24
26
|
dialect?: () => string
|
|
25
27
|
/** 单一 DB 事务包裹(P3-3):回调内语句共享一个 BEGIN/COMMIT/ROLLBACK 边界,异常即整段回滚 */
|
|
26
|
-
tx: <T>(fn: (t: { exec: (sql: string, params?: unknown[]) => Promise<unknown>, query: (sql: string, params?: unknown[]) => Promise<unknown
|
|
28
|
+
tx: <T>(fn: (t: { exec: (sql: string, params?: unknown[]) => Promise<unknown>, query: (sql: string, params?: unknown[]) => Promise<unknown>, table: (name: string) => any }) => Promise<T>) => Promise<T>
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
function isObj(v: unknown): v is Record<string, unknown> {
|
|
@@ -83,6 +85,7 @@ export function catalogSpec(dialect: string): { existing: string, like: (pat: st
|
|
|
83
85
|
|
|
84
86
|
/** 当前库实际存在的表名集合 */
|
|
85
87
|
async function listExistingTables(dialect: string): Promise<Set<string>> {
|
|
88
|
+
// 保留 db.query:方言相关表目录查询(sqlite_master / information_schema),构造器无法表达
|
|
86
89
|
const rows = (await db.query(catalogSpec(dialect).existing)) as Array<{ name: string }>;
|
|
87
90
|
return new Set(rows.map(r => r.name));
|
|
88
91
|
}
|
|
@@ -110,6 +113,7 @@ export async function collectRetainedTables(name: string, manifestPath: string):
|
|
|
110
113
|
}
|
|
111
114
|
// ② 前缀兜底:扫描当前库匹配模块前缀的表,覆盖未声明场景
|
|
112
115
|
try {
|
|
116
|
+
// 保留 db.query:LIKE ? ESCAPE '!' 转义子句,构造器 like 无 ESCAPE 支持
|
|
113
117
|
const [sql, params] = catalogSpec(dialect).like(tablePrefixPattern(name));
|
|
114
118
|
const rows = (await db.query(sql, params)) as Array<{ name: string }>;
|
|
115
119
|
for (const r of rows)
|
|
@@ -155,10 +159,11 @@ export interface UninstallResult {
|
|
|
155
159
|
/** 计算卸载影响面(角色数 + 持有这些角色的用户数) */
|
|
156
160
|
export async function computeUninstallImpact(input: UninstallInput): Promise<UninstallImpact> {
|
|
157
161
|
const { name } = input;
|
|
158
|
-
const roles = await db.
|
|
162
|
+
const roles = await db.table("sys_role").select(["name"]).where({ field: "module", op: "eq", value: name }).all() as Array<{ name: string }>;
|
|
159
163
|
const roleNames = roles.map(r => r.name);
|
|
160
164
|
let affectedUsers = 0;
|
|
161
165
|
if (roleNames.length > 0) {
|
|
166
|
+
// 保留 db.query:IN 占位符经 JS 字符串拼接动态生成(动态 SQL),构造器 in 仅接受静态数组值
|
|
162
167
|
const rows = await db.query(
|
|
163
168
|
`select distinct user_id from sys_user_role where role_name in (${roleNames.map(() => "?").join(",")})`,
|
|
164
169
|
roleNames,
|
|
@@ -202,7 +207,7 @@ export async function runUninstall(input: UninstallInput): Promise<UninstallResu
|
|
|
202
207
|
// 边界。任一步失败 → 整段回滚(无角色/权限/路由半残),锁不摘(fail-closed:模块仍挂载、可重试或重装自愈)。
|
|
203
208
|
try {
|
|
204
209
|
await db.tx(async (t) => {
|
|
205
|
-
await t.
|
|
210
|
+
await t.table("sys_solution_route").where({ field: "module", op: "eq", value: name }).delete().run();
|
|
206
211
|
await uninstallSolution(name, t);
|
|
207
212
|
});
|
|
208
213
|
}
|
|
@@ -213,7 +218,7 @@ export async function runUninstall(input: UninstallInput): Promise<UninstallResu
|
|
|
213
218
|
|
|
214
219
|
// ② sidecar 删条目(patch 值 null = 删除该方案键)
|
|
215
220
|
try {
|
|
216
|
-
await input.state.write({ solutions: { [name]: null } });
|
|
221
|
+
await input.state.write({ solutions: { [name]: null } } as unknown as Parameters<typeof input.state.write>[0]);
|
|
217
222
|
}
|
|
218
223
|
catch (e) {
|
|
219
224
|
audit(false, `sidecar 写失败:${String(e)}`);
|
|
@@ -206,12 +206,12 @@ export async function diffWebIntegrity(
|
|
|
206
206
|
const bytes = await fsFace().readFile(`${distRoot}/${me.entry}`);
|
|
207
207
|
const actual = `sha384-${sha384(new Uint8Array(bytes))}`;
|
|
208
208
|
if (actual !== me.integrity) {
|
|
209
|
-
items.push({ id: "web", severity: "error", subject: me.entry, advise_key: "lifecycle.verify.web.integrity_mismatch" });
|
|
209
|
+
items.push({ id: "web", severity: "error", subject: me.entry ?? "*", advise_key: "lifecycle.verify.web.integrity_mismatch" });
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
212
|
catch {
|
|
213
213
|
// 资产缺失(web 入口未落位或路径变更)——info 非阻断(路由可达性已另测)
|
|
214
|
-
items.push({ id: "web", severity: "info", subject: me.entry, advise_key: "lifecycle.verify.web.asset_missing" });
|
|
214
|
+
items.push({ id: "web", severity: "info", subject: me.entry ?? "*", advise_key: "lifecycle.verify.web.asset_missing" });
|
|
215
215
|
}
|
|
216
216
|
return items;
|
|
217
217
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// 目录红线:只出 id/username/display_name——不含 email/avatar/roles(防低权账号拖走全员邮箱喂钓鱼,安全评审 P2);
|
|
3
3
|
// 详情端点才给 email/avatar。默认排除 is_forbidden=1(AC1-23:管理域仍可见,目录与管理分离)。
|
|
4
4
|
// keyword≥2、LIKE 转义复用 query.ts;solutions 同 isolate import searchContacts 直调,无 HTTP 往返(AC1-25)。
|
|
5
|
-
import { likePattern } from "
|
|
5
|
+
import { likePattern } from "../infra/query";
|
|
6
6
|
|
|
7
7
|
export interface ContactRow {
|
|
8
8
|
id: number
|
|
@@ -17,19 +17,21 @@ export async function searchContacts(keyword: unknown, opts: { limit?: unknown }
|
|
|
17
17
|
const pattern = likePattern(keyword); // <2 字符在此抛 400(AC1-22)
|
|
18
18
|
const rawLimit = Number.parseInt(String(opts.limit ?? ""), 10);
|
|
19
19
|
const limit = Number.isFinite(rawLimit) && rawLimit >= 1 ? Math.min(MAX_LIMIT, rawLimit) : DEFAULT_LIMIT;
|
|
20
|
+
// 保留 db.query:模糊搜索需 ESCAPE '!'(构造器 like 无 escape 子句;mysql 默认 sql_mode 下 ESCAPE '\' 即 1064)
|
|
20
21
|
return await db.query(
|
|
21
22
|
`select id, username, display_name from users
|
|
22
23
|
where is_forbidden = 0 and (username like ? escape '!' or display_name like ? escape '!')
|
|
23
24
|
order by username limit ?`,
|
|
24
25
|
[pattern, pattern, limit],
|
|
25
|
-
) as ContactRow[];
|
|
26
|
+
) as unknown as ContactRow[];
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export async function contactDetail(id: number): Promise<Record<string, unknown>> {
|
|
29
|
-
const rows = await db.
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
const rows = await db.table("users")
|
|
31
|
+
.select(["id", "username", "display_name", "email", "avatar_base64"])
|
|
32
|
+
.where({ field: "id", op: "eq", value: id })
|
|
33
|
+
.where({ field: "is_forbidden", op: "eq", value: 0 })
|
|
34
|
+
.all();
|
|
33
35
|
if (!rows[0])
|
|
34
36
|
throw Object.assign(new Error("contact not found"), { status: 404 });
|
|
35
37
|
return rows[0] as Record<string, unknown>;
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// 规则锚:custom 机器名强制(AC1-17)、builtin 角色绑定/删除硬拒(AC1-19)、占用禁删(AC1-20)、
|
|
3
3
|
// 绑定调整 = 紧急吊销出口(AC1-30,写后按持有者失效判权缓存)、写审计 await recordOp(AC1-28 关键路径)。
|
|
4
4
|
// 判权本身不经过这里——调用 handler 先过 assertSystemAdmin(H2,管理类端点)。
|
|
5
|
-
import { invalidateUser } from "
|
|
6
|
-
import { recordOp } from "
|
|
5
|
+
import { invalidateUser } from "../infra/access";
|
|
6
|
+
import { recordOp } from "../infra/audit";
|
|
7
7
|
|
|
8
8
|
export interface RoleRow {
|
|
9
9
|
name: string
|
|
@@ -35,7 +35,7 @@ const CUSTOM_NAME = /^custom\.[a-z0-9][a-z0-9-]*$/;
|
|
|
35
35
|
const RESERVED = new Set(["superadmin", "admin", "editor", "viewer"]);
|
|
36
36
|
|
|
37
37
|
async function assertNotBuiltin(roleName: string): Promise<void> {
|
|
38
|
-
const rows = await db.
|
|
38
|
+
const rows = await db.table("sys_role").select(["builtin"]).where({ field: "name", op: "eq", value: roleName }).all();
|
|
39
39
|
if (!rows[0])
|
|
40
40
|
fail(404, `role not found: ${roleName}`);
|
|
41
41
|
if (Number((rows[0] as { builtin: unknown }).builtin) === 1)
|
|
@@ -45,9 +45,8 @@ async function assertNotBuiltin(roleName: string): Promise<void> {
|
|
|
45
45
|
async function assertCodesExist(codes: string[]): Promise<void> {
|
|
46
46
|
if (!codes.length)
|
|
47
47
|
return;
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const known = new Set(rows.map((r: { code: string }) => r.code));
|
|
48
|
+
const rows = await db.table("sys_permission").select(["code"]).where({ field: "code", op: "in", value: codes }).all();
|
|
49
|
+
const known = new Set(rows.map(r => (r as { code: string }).code));
|
|
51
50
|
const unknown = codes.filter(c => !known.has(c));
|
|
52
51
|
if (unknown.length)
|
|
53
52
|
fail(400, `unknown permission codes: ${unknown.join(", ")}`);
|
|
@@ -55,15 +54,15 @@ async function assertCodesExist(codes: string[]): Promise<void> {
|
|
|
55
54
|
|
|
56
55
|
/** 绑定/删除写后按持有者逐人失效判权缓存(D10 撤销即强退的 service 级出口)。 */
|
|
57
56
|
async function invalidateHolders(roleName: string): Promise<void> {
|
|
58
|
-
const rows = await db.
|
|
57
|
+
const rows = await db.table("sys_user_role").select(["user_id"]).where({ field: "role_name", op: "eq", value: roleName }).all();
|
|
59
58
|
for (const r of rows as Array<{ user_id: number }>)
|
|
60
59
|
invalidateUser(Number(r.user_id));
|
|
61
60
|
}
|
|
62
61
|
|
|
63
62
|
export async function listRoles(): Promise<RoleRow[]> {
|
|
64
|
-
const roles = await db.
|
|
65
|
-
const holders = await db.
|
|
66
|
-
const binds = await db.
|
|
63
|
+
const roles = await db.table("sys_role").select(["name", "module", "title", "description", "builtin", "deprecated"]).orderBy([{ field: "name" }]).all() as Array<Record<string, unknown>>;
|
|
64
|
+
const holders = await db.table("sys_user_role").select(["role_name", { fn: "count", as: "c" }]).groupBy(["role_name"]).all() as Array<{ role_name: string, c: number }>;
|
|
65
|
+
const binds = await db.table("sys_role_permission").select(["role_name", "permission_code"]).orderBy([{ field: "permission_code" }]).all() as Array<{ role_name: string, permission_code: string }>;
|
|
67
66
|
const holderMap = new Map(holders.map(h => [h.role_name, Number(h.c)]));
|
|
68
67
|
const bindMap = new Map<string, string[]>();
|
|
69
68
|
for (const b of binds)
|
|
@@ -81,7 +80,7 @@ export async function listRoles(): Promise<RoleRow[]> {
|
|
|
81
80
|
}
|
|
82
81
|
|
|
83
82
|
export async function listPermissions(): Promise<PermissionRow[]> {
|
|
84
|
-
const rows = await db.
|
|
83
|
+
const rows = await db.table("sys_permission").select(["code", "module", "title_key", "deprecated"]).orderBy([{ field: "code" }]).all() as Array<Record<string, unknown>>;
|
|
85
84
|
return rows.map(r => ({
|
|
86
85
|
code: String(r.code),
|
|
87
86
|
module: String(r.module),
|
|
@@ -97,9 +96,9 @@ export async function createRole(input: { name: string, title: string, permissio
|
|
|
97
96
|
const permissions = [...new Set((input.permissions ?? []).map(String))];
|
|
98
97
|
await assertCodesExist(permissions);
|
|
99
98
|
await db.tx(async (t) => {
|
|
100
|
-
await t.
|
|
99
|
+
await t.table("sys_role").insert({ name, module: "custom", title: String(input.title ?? ""), builtin: 0, deprecated: 0 }).run();
|
|
101
100
|
for (const code of permissions)
|
|
102
|
-
await t.
|
|
101
|
+
await t.table("sys_role_permission").insert({ role_name: name, permission_code: code }).run();
|
|
103
102
|
});
|
|
104
103
|
await recordOp({ action: "role.create", module: "framework", target: name, detail: { title: String(input.title ?? ""), permissions } });
|
|
105
104
|
return { name, module: "custom" };
|
|
@@ -109,12 +108,12 @@ export async function updateBindings(roleName: string, codes: string[]): Promise
|
|
|
109
108
|
await assertNotBuiltin(roleName);
|
|
110
109
|
const next = [...new Set(codes.map(String))];
|
|
111
110
|
await assertCodesExist(next);
|
|
112
|
-
const before = (await db.
|
|
111
|
+
const before = (await db.table("sys_role_permission").select(["permission_code"]).where({ field: "role_name", op: "eq", value: roleName }).all()) as Array<{ permission_code: string }>;
|
|
113
112
|
const beforeSet = new Set(before.map(b => b.permission_code));
|
|
114
113
|
await db.tx(async (t) => {
|
|
115
|
-
await t.
|
|
114
|
+
await t.table("sys_role_permission").where({ field: "role_name", op: "eq", value: roleName }).delete().run();
|
|
116
115
|
for (const code of next)
|
|
117
|
-
await t.
|
|
116
|
+
await t.table("sys_role_permission").insert({ role_name: roleName, permission_code: code }).run();
|
|
118
117
|
});
|
|
119
118
|
await invalidateHolders(roleName);
|
|
120
119
|
const added = next.filter(c => !beforeSet.has(c));
|
|
@@ -124,13 +123,13 @@ export async function updateBindings(roleName: string, codes: string[]): Promise
|
|
|
124
123
|
|
|
125
124
|
export async function deleteRole(roleName: string): Promise<void> {
|
|
126
125
|
await assertNotBuiltin(roleName);
|
|
127
|
-
const holders = await db.
|
|
126
|
+
const holders = await db.table("sys_user_role").select([{ fn: "count", as: "c" }]).where({ field: "role_name", op: "eq", value: roleName }).all();
|
|
128
127
|
const n = Number((holders[0] as { c: unknown }).c);
|
|
129
128
|
if (n > 0)
|
|
130
129
|
fail(409, `role has ${n} holder(s)`, { holders: n });
|
|
131
130
|
await db.tx(async (t) => {
|
|
132
|
-
await t.
|
|
133
|
-
await t.
|
|
131
|
+
await t.table("sys_role_permission").where({ field: "role_name", op: "eq", value: roleName }).delete().run();
|
|
132
|
+
await t.table("sys_role").where({ field: "name", op: "eq", value: roleName }).delete().run();
|
|
134
133
|
});
|
|
135
134
|
await invalidateHolders(roleName);
|
|
136
135
|
await recordOp({ action: "role.delete", module: "framework", target: roleName });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// 系统设置(PRD §4.3)+ 登录方式发现(C5/D2)。
|
|
2
2
|
// secret 项读取掩码(***);market.* 安全项写操作须 superadmin 操作者(D11 边界在 service 按 actor 角色查)。
|
|
3
|
-
import { recordOp } from "
|
|
3
|
+
import { recordOp } from "../infra/audit";
|
|
4
4
|
|
|
5
5
|
export interface SettingItem {
|
|
6
6
|
key: string
|
|
@@ -29,7 +29,7 @@ export interface SettingGroup {
|
|
|
29
29
|
const SUPER_ONLY_GROUPS = new Set(["security"]);
|
|
30
30
|
|
|
31
31
|
export async function listSettings(): Promise<SettingGroup[]> {
|
|
32
|
-
const rows = await db.
|
|
32
|
+
const rows = await db.table("sys_setting").select(["key", "module", "group", "type", "value", "default_value", "label_key", "group_label_key", "options", "secret", "update_time"]).orderBy([{ field: "group" }, { field: "key" }]).all() as Array<Record<string, unknown>>;
|
|
33
33
|
const groups = new Map<string, SettingItem[]>();
|
|
34
34
|
for (const r of rows) {
|
|
35
35
|
const item: SettingItem = {
|
|
@@ -55,22 +55,22 @@ export async function listSettings(): Promise<SettingGroup[]> {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export async function updateSetting(actorId: number, key: string, value: string): Promise<void> {
|
|
58
|
-
const rows = await db.
|
|
58
|
+
const rows = await db.table("sys_setting").select(["key", "module", "group", "type", "value", "default_value", "label_key", "group_label_key", "options", "secret", "update_time"]).where({ field: "key", op: "eq", value: key }).all() as Array<Record<string, unknown>>;
|
|
59
59
|
if (!rows.length)
|
|
60
60
|
throw Object.assign(new Error(`setting not found: ${key}`), { status: 404 });
|
|
61
61
|
const row = rows[0];
|
|
62
62
|
if (SUPER_ONLY_GROUPS.has(String(row.group))) {
|
|
63
|
-
const actorRoles = await db.
|
|
63
|
+
const actorRoles = await db.table("sys_user_role").select(["role_name"]).where({ field: "user_id", op: "eq", value: actorId }).all() as Array<{ role_name: string }>;
|
|
64
64
|
if (!actorRoles.some(r => r.role_name === "superadmin"))
|
|
65
65
|
throw Object.assign(new Error("security settings require superadmin"), { status: 403 });
|
|
66
66
|
}
|
|
67
|
-
await db.
|
|
67
|
+
await db.table("sys_setting").update({ value: String(value ?? ""), update_time: Date.now() }).where({ field: "key", op: "eq", value: key }).run();
|
|
68
68
|
await recordOp({ action: "setting.update", module: String(row.module), target: key });
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
/** C5/D2:登录方式发现的数据源——local 恒在,ldap/sso 由设置项开关。 */
|
|
72
72
|
export async function listLoginMethods(): Promise<{ methods: string[] }> {
|
|
73
|
-
const rows = await db.
|
|
73
|
+
const rows = await db.table("sys_setting").select(["key", "value"]).where({ field: "key", op: "in", value: ["ldap.enabled", "sso.enabled"] }).all() as Array<{ key: string, value: string | null }>;
|
|
74
74
|
const enabled = (k: string) => rows.find(r => r.key === k)?.value === "1";
|
|
75
75
|
return { methods: ["local", ...(enabled("ldap.enabled") ? ["ldap" as const] : []), ...(enabled("sso.enabled") ? ["sso" as const] : [])] };
|
|
76
76
|
}
|
package/templates/api/src/_platform/_shared/service/{solution-perms.ts → domain/solution-perms.ts}
RENAMED
|
@@ -28,20 +28,20 @@ export async function upsertSolutionPerms(m: SolutionManifest): Promise<UpsertRe
|
|
|
28
28
|
const declaredPerms = new Set(m.roles.flatMap(r => r.permissions ?? []));
|
|
29
29
|
|
|
30
30
|
for (const role of m.roles) {
|
|
31
|
-
const existing = await db.
|
|
31
|
+
const existing = await db.table("sys_role").select(["name"]).where({ field: "name", op: "eq", value: role.name }).all() as unknown[];
|
|
32
32
|
// upsert:行存在只覆盖元数据(title),name/builtin 不动(AC2-2 元数据覆盖)
|
|
33
33
|
if (existing.length) {
|
|
34
|
-
await db.
|
|
34
|
+
await db.table("sys_role").update({ title: role.title }).where({ field: "name", op: "eq", value: role.name }).run();
|
|
35
35
|
}
|
|
36
36
|
else {
|
|
37
|
-
await db.
|
|
37
|
+
await db.table("sys_role").insert({ name: role.name, module: m.module, title: role.title, builtin: 0 }).run();
|
|
38
38
|
// 声明绑定只在角色创建时写一次(角色级 insert-if-absent,AC1-15 红线):
|
|
39
39
|
// 升级/重装对已存在角色永不补写声明绑定——管理员撤过的绑定不复活;
|
|
40
40
|
// 新版本给既有角色新增权限点时,由管理员显式勾选(insert-if-absent 的取舍边界,§9.5)。
|
|
41
41
|
for (const code of role.permissions ?? []) {
|
|
42
|
-
const bound = await db.
|
|
42
|
+
const bound = await db.table("sys_role_permission").select(["role_name"]).where({ field: "role_name", op: "eq", value: role.name }).where({ field: "permission_code", op: "eq", value: code }).all() as unknown[];
|
|
43
43
|
if (!bound.length)
|
|
44
|
-
await db.
|
|
44
|
+
await db.table("sys_role_permission").insert({ role_name: role.name, permission_code: code }).run();
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
}
|
|
@@ -49,24 +49,24 @@ export async function upsertSolutionPerms(m: SolutionManifest): Promise<UpsertRe
|
|
|
49
49
|
// 权限点行独立于角色创建:声明里的新 code 一律 upsert 插入(AC2-1「新增声明 → upsert 插入新行」——
|
|
50
50
|
// 含既有角色新增权限点的场景;绑定是否自动挂上由上面的角色级规则决定)
|
|
51
51
|
for (const code of declaredPerms) {
|
|
52
|
-
const p = await db.
|
|
52
|
+
const p = await db.table("sys_permission").select(["code"]).where({ field: "code", op: "eq", value: code }).all() as unknown[];
|
|
53
53
|
if (!p.length)
|
|
54
|
-
await db.
|
|
54
|
+
await db.table("sys_permission").insert({ code, module: m.module }).run();
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
// 声明消失 → 行保留标 deprecated(AC2-3/4):行与绑定全留,判决不突变;
|
|
58
58
|
// 重命名的旧 code 走同一条规则(视为删除+新增),deprecated 清单交给安装器告警。
|
|
59
|
-
const oldPerms = await db.
|
|
59
|
+
const oldPerms = await db.table("sys_permission").select(["code"]).where({ field: "module", op: "eq", value: m.module }).where({ field: "deprecated", op: "eq", value: 0 }).all() as Array<{ code: string }>;
|
|
60
60
|
for (const r of oldPerms) {
|
|
61
61
|
if (!declaredPerms.has(String(r.code))) {
|
|
62
|
-
await db.
|
|
62
|
+
await db.table("sys_permission").update({ deprecated: 1 }).where({ field: "code", op: "eq", value: r.code }).run();
|
|
63
63
|
deprecated.push(String(r.code));
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
-
const oldRoles = await db.
|
|
66
|
+
const oldRoles = await db.table("sys_role").select(["name"]).where({ field: "module", op: "eq", value: m.module }).where({ field: "builtin", op: "eq", value: 0 }).where({ field: "deprecated", op: "eq", value: 0 }).all() as Array<{ name: string }>;
|
|
67
67
|
for (const r of oldRoles) {
|
|
68
68
|
if (!declaredRoles.has(String(r.name))) {
|
|
69
|
-
await db.
|
|
69
|
+
await db.table("sys_role").update({ deprecated: 1 }).where({ field: "name", op: "eq", value: r.name }).run();
|
|
70
70
|
deprecated.push(String(r.name));
|
|
71
71
|
}
|
|
72
72
|
}
|
|
@@ -77,6 +77,7 @@ export async function upsertSolutionPerms(m: SolutionManifest): Promise<UpsertRe
|
|
|
77
77
|
interface DbLike {
|
|
78
78
|
query: (sql: string, params?: unknown[]) => Promise<unknown>
|
|
79
79
|
exec: (sql: string, params?: unknown[]) => Promise<unknown>
|
|
80
|
+
table: DBInstance["table"]
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
/**
|
|
@@ -88,14 +89,14 @@ interface DbLike {
|
|
|
88
89
|
* P3-3:可传入事务句柄 `d`(db.tx 的 t),使级联纳入卸载的单一 DB 事务;缺省回落全局 db(兼容既有调用面)。
|
|
89
90
|
*/
|
|
90
91
|
export async function uninstallSolution(module: string, d: DbLike = db): Promise<void> {
|
|
91
|
-
const roles = await d.
|
|
92
|
+
const roles = await d.table("sys_role").select(["name"]).where({ field: "module", op: "eq", value: module }).all() as Array<{ name: string }>;
|
|
92
93
|
for (const r of roles) {
|
|
93
|
-
await d.
|
|
94
|
-
await d.
|
|
94
|
+
await d.table("sys_role_permission").where({ field: "role_name", op: "eq", value: r.name }).delete().run();
|
|
95
|
+
await d.table("sys_user_role").where({ field: "role_name", op: "eq", value: r.name }).delete().run();
|
|
95
96
|
}
|
|
96
|
-
await d.
|
|
97
|
-
//
|
|
97
|
+
await d.table("sys_role").where({ field: "module", op: "eq", value: module }).delete().run();
|
|
98
|
+
// 保留 d.exec:LIKE ? escape '!' 转义子句构造器 like 不支持(三方言 ESCAPE 同义纪律见 query.ts)
|
|
98
99
|
const prefix = `${module.replace(/[!%_]/g, c => `!${c}`)}.%`;
|
|
99
100
|
await d.exec("delete from sys_role_permission where permission_code like ? escape '!'", [prefix]);
|
|
100
|
-
await d.
|
|
101
|
+
await d.table("sys_permission").where({ field: "module", op: "eq", value: module }).delete().run();
|
|
101
102
|
}
|