@oj-module/cli 0.2.6 → 0.2.14
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/README.md +70 -24
- package/devkit/framework-dev.md +14 -5
- package/devkit/manual.md +28 -23
- package/devkit/module-dev-and-cdn-playground.md +197 -0
- package/devkit/sql-builder-missing-capabilities-pr.md +268 -0
- package/package.json +2 -2
- package/shell-dist/assets/runtime.js +130 -45
- package/shell-dist/index.html +2 -2
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/build-backend.test.ts +64 -0
- package/src/__tests__/versions.test.ts +26 -0
- package/src/build.ts +9 -0
- package/src/bundle.ts +8 -5
- package/src/contract/__tests__/handler-scan.test.ts +29 -0
- package/src/contract/__tests__/market-client.integration.test.ts +170 -0
- package/src/contract/handler-scan.ts +35 -4
- package/src/dev.ts +10 -2
- package/src/index.ts +9 -3
- package/src/init.ts +24 -0
- package/src/lifecycle/__tests__/bundle.test.ts +3 -3
- package/src/market/__tests__/bootstrap.test.ts +204 -0
- package/src/market/__tests__/build-index.test.ts +154 -0
- package/src/market/__tests__/cdn-layout.test.ts +96 -0
- package/src/market/__tests__/failure-playbook.test.ts +33 -0
- package/src/market/__tests__/fixture.test.ts +48 -0
- package/src/market/__tests__/publish.test.ts +109 -0
- package/src/market/__tests__/report.test.ts +51 -0
- package/src/market/__tests__/setup.test.ts +114 -0
- package/src/market/__tests__/verify.test.ts +111 -0
- package/src/market/bootstrap.ts +255 -0
- package/src/market/build-index.ts +159 -0
- package/src/market/cdn-layout.ts +122 -0
- package/src/market/command.ts +139 -0
- package/src/market/failure-playbook.ts +57 -0
- package/src/market/fixture.ts +138 -0
- package/src/market/index-builder.ts +76 -0
- package/src/market/publish.ts +156 -0
- package/src/market/report.ts +67 -0
- package/src/market/scan.ts +99 -0
- package/src/market/setup.ts +144 -0
- package/src/market/trial.ts +121 -0
- package/src/market/verify.ts +298 -0
- package/src/market/walkthrough.ts +506 -0
- package/src/oj.ts +3 -1
- package/src/preview-config.ts +177 -0
- package/src/preview.ts +106 -56
- package/src/sign.ts +21 -6
- package/src/usage.ts +22 -2
- package/src/vendor.ts +4 -2
- package/src/versions.ts +28 -0
- package/templates/api/.ojm-api-exempt.json +6 -2
- package/templates/api/config.yaml +24 -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/extract.ts +8 -8
- 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 +29 -56
- package/templates/api/src/_platform/_lifecycle/verify-checks.ts +2 -2
- package/templates/api/src/_platform/_shared/service/{contacts.ts → domain/contacts.ts} +20 -13
- 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} +25 -22
- 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/infra/oplog.ts +40 -0
- package/templates/api/src/_platform/_shared/service/{query.ts → infra/query.ts} +18 -0
- package/templates/api/src/_platform/_shared/service/{rate.ts → infra/rate.ts} +16 -13
- 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/fixtures/l1-test-user.sql +13 -0
- package/templates/api/src/_platform/manifest.yaml +4 -1
- package/templates/api/src/_platform/market/api.ts +54 -16
- package/templates/api/src/_platform/migrations/0013__users_profile.sql +4 -0
- package/templates/api/src/_platform/schema.yaml +26 -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 +26 -22
- package/templates/api/src/auth/api.ts +1 -1
- package/templates/api/src/demo/todos/api.ts +8 -8
- 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 +97 -57
- package/templates/api/src/notification/api.ts +1 -1
- package/templates/api/src/notification/manifest.yaml +1 -0
- package/templates/api/src/notification/migrations/0004__notifications_broadcast_hidden.sql +12 -0
- package/templates/api/src/notification/schema.yaml +7 -0
- package/templates/api/src/notification/seed.sql +5 -4
- package/templates/api/src/personal-center/profile/api.ts +63 -0
- 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 +4 -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/fixture.ts +8 -16
- package/templates/api/tests/guard.spec.ts +1 -1
- package/templates/api/tests/l1-builder.test.ts +91 -0
- package/templates/api/tests/lifecycle/installer.spec.ts +1 -4
- package/templates/api/tests/lifecycle/market-api.spec.ts +2 -4
- package/templates/api/tests/lifecycle/steps-fs.spec.ts +5 -8
- package/templates/api/tests/lifecycle/trust.spec.ts +1 -1
- package/templates/api/tests/lifecycle/uninstall.spec.ts +29 -89
- package/templates/api/tests/query-utils.spec.ts +85 -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 +66 -21
- 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/cdn/README.md +48 -0
- package/templates/env.d.ts +2 -0
- package/templates/global.d.ts +850 -0
- package/templates/init.include.yaml +14 -2
- package/templates/logs/task-events.jsonl +0 -0
- package/templates/openapi.json +580 -23
- 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/notification/entry.ts +46 -9
- package/templates/web/src/notification/locales/en-US.json +19 -0
- package/templates/web/src/notification/locales/zh-CN.json +19 -0
- package/templates/web/src/notification/pages/center/index.tsx +131 -0
- 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/entry.ts +8 -2
- 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/src/personal-center/__client/api.schemas.ts +9 -0
- package/templates/web/src/personal-center/__client/api.ts +25 -1
- package/templates/web/src/personal-center/entry.ts +5 -0
- package/templates/web/src/personal-center/pages/my-profile/index.tsx +25 -3
- package/templates/web/src/system/entry.ts +2 -1
- package/templates/web.config.ts +13 -10
- package/vendor/host-versions.json +3 -3
- package/templates/api/src/_platform/_shared/service/oplog.ts +0 -47
- package/templates/api/src/notifications/api.ts +0 -32
- package/templates/api/src/notifications/manifest.yaml +0 -5
- package/templates/web/src/notifications/__client/api.schemas.ts +0 -8
- package/templates/web/src/notifications/__client/api.ts +0 -80
- /package/templates/api/src/_platform/_shared/service/{guard.ts → infra/guard.ts} +0 -0
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// 参数化 insert,与 audit.ts 同纪律(值不拼字符串)。
|
|
6
6
|
|
|
7
7
|
// db 为 oj isolate 注入全局(同模板 service 层惯例,installer.ts 同款;测试由夹具装 globalThis.db)
|
|
8
|
-
declare const db: { exec: (sql: string, params: unknown[]) => Promise<unknown
|
|
8
|
+
declare const db: { exec: (sql: string, params: unknown[]) => Promise<unknown>, table: (name: string) => any };
|
|
9
9
|
|
|
10
10
|
export interface HistoryRow {
|
|
11
11
|
solution: string
|
|
@@ -20,10 +20,15 @@ export interface HistoryRow {
|
|
|
20
20
|
/** 守卫式 insert:表不存在/写失败一律静默(留痕失败不影响主流程) */
|
|
21
21
|
export async function recordHistory(row: HistoryRow): Promise<void> {
|
|
22
22
|
try {
|
|
23
|
-
await db.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
await db.table("market_history").insert({
|
|
24
|
+
solution: row.solution,
|
|
25
|
+
version: row.version,
|
|
26
|
+
action: row.action,
|
|
27
|
+
status: row.status,
|
|
28
|
+
operator: row.operator,
|
|
29
|
+
message: row.message,
|
|
30
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
31
|
+
}).run();
|
|
27
32
|
}
|
|
28
33
|
catch {
|
|
29
34
|
// 表不存在(市场方案未装)或写失败:静默跳过,不阻断主流程
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// setEnabled 四处同写(D12/AC-P1-6):锁摘除/恢复 + sidecar + sys_solution_route.enabled +
|
|
15
15
|
// modules.json enabled;启用从 sidecar 摘除前记录恢复版本。db 为 isolate 全局(同模板 service 层)。
|
|
16
16
|
|
|
17
|
-
import type { OpEntry } from "../_shared/service/audit";
|
|
17
|
+
import type { OpEntry } from "../_shared/service/infra/audit";
|
|
18
18
|
import type { TaskLock } from "./lock";
|
|
19
19
|
import type { StateStore } from "./state";
|
|
20
20
|
import type { FetchFace } from "./steps/download";
|
|
@@ -22,7 +22,7 @@ import type { ExecFace } from "./steps/extract";
|
|
|
22
22
|
import type { SettingDecl, SolutionDecl, SolutionSnapshot } from "./steps/register";
|
|
23
23
|
import type { ReloadFace } from "./steps/reload";
|
|
24
24
|
import type { InstallContext, InstallStage, StageResult, TaskRecord, TaskStatus } from "./types";
|
|
25
|
-
import { auditOp } from "../_shared/service/audit";
|
|
25
|
+
import { auditOp } from "../_shared/service/infra/audit";
|
|
26
26
|
import { atomicWriteText, fsFace, randomId } from "./atomic";
|
|
27
27
|
import { recordHistory } from "./history";
|
|
28
28
|
import { acquireTaskLock, commitLockEntry, readLock, writeLock } from "./lock";
|
|
@@ -40,7 +40,7 @@ import { appendAnonymousPaths, bumpInstallSeq } from "./text-edit";
|
|
|
40
40
|
import { createTrustStore } from "./trust";
|
|
41
41
|
|
|
42
42
|
// db/http 为 oj isolate 注入全局(同模板 service 层惯例,register.ts/audit.ts 同款;测试由夹具装 globalThis.db)
|
|
43
|
-
declare const db: { exec: (sql: string, params: unknown[]) => Promise<unknown
|
|
43
|
+
declare const db: { exec: (sql: string, params: unknown[]) => Promise<unknown>, table: (name: string) => any };
|
|
44
44
|
|
|
45
45
|
export type { InstallContext, InstallStage, StageResult, TaskRecord, TaskStatus };
|
|
46
46
|
|
|
@@ -1039,7 +1039,7 @@ export async function setEnabled(input: SetEnabledInput): Promise<{ ok: boolean,
|
|
|
1039
1039
|
}
|
|
1040
1040
|
// ③ route 表 enabled(生命周期开关,非注册内容直写,D10 不适用)
|
|
1041
1041
|
try {
|
|
1042
|
-
await db.
|
|
1042
|
+
await db.table("sys_solution_route").update({ enabled: enabled ? 1 : 0 }).where({ field: "module", op: "eq", value: name }).run();
|
|
1043
1043
|
}
|
|
1044
1044
|
catch (e) {
|
|
1045
1045
|
audit(false, `route enabled 写失败:${String(e)}`);
|
|
@@ -101,7 +101,7 @@ export function sha384Bytes(data: Uint8Array): Uint8Array {
|
|
|
101
101
|
view.setUint32(padded - 4, bitLenLo);
|
|
102
102
|
|
|
103
103
|
const h: Array<[number, number]> = IV.map(x => [x[0], x[1]] as [number, number]);
|
|
104
|
-
const w = Array.from({ length: 80 });
|
|
104
|
+
const w = Array.from({ length: 80 }, () => [0, 0] as [number, number]);
|
|
105
105
|
for (let off = 0; off < padded; off += 128) {
|
|
106
106
|
for (let t = 0; t < 16; t++) {
|
|
107
107
|
w[t] = [view.getUint32(off + t * 8), view.getUint32(off + t * 8 + 4)];
|
|
@@ -39,7 +39,7 @@ export function archiveFileOf(manifest: unknown): { ok: true, file: string, arch
|
|
|
39
39
|
if (typeof manifest !== "object" || manifest === null || Array.isArray(manifest)) {
|
|
40
40
|
return { ok: false, message: "C2-29:manifest 不是键值对象,archive 身份不可得" };
|
|
41
41
|
}
|
|
42
|
-
const archive = (manifest as Record<string, unknown>).archive
|
|
42
|
+
const archive = (manifest as Record<string, unknown>).archive as Record<string, unknown>;
|
|
43
43
|
if (typeof archive !== "object" || archive === null || Array.isArray(archive)) {
|
|
44
44
|
return { ok: false, message: "C2-29:archive 段缺失或不是键值对象(Form B 真源须含 file/sha256/size)" };
|
|
45
45
|
}
|
|
@@ -122,10 +122,13 @@ const NATIVE_KEYS = new Set(["name", "desc", "version", "config", "db", "tables"
|
|
|
122
122
|
/**
|
|
123
123
|
* C2-31 安装期复核纯函数(双端 DRY 于夹具,preflight 同纪律;构建侧同形实现在
|
|
124
124
|
* packages/cli/src/bundle.ts checkModuleManifest):包内 oj 原生清单 ↔ C2 Form A 逐项钉住——
|
|
125
|
-
* name/version
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
125
|
+
* name/version 三方一致、七键白名单(白名单外键 = OJ-X 红线,oj 不拒但契约拒)。
|
|
126
|
+
* 只验不写:安装期改写原生清单等于掩盖上游不一致。
|
|
127
|
+
*
|
|
128
|
+
* 变更(2026-10-21):**desc === summary 的等值校验已移除**。该等值要求把「构建期映射」
|
|
129
|
+
* 当成安装期契约,于是 fixture/手写包只要 desc 与 summary 有一字之差就整包装不上——
|
|
130
|
+
* 校验成本远高于它拦下的风险(desc 只是模块描述文本,非安全边界)。现只保留
|
|
131
|
+
* 「summary 必填(C2-04)」与「desc 必填非空(oj 启动装配面)」两项。
|
|
129
132
|
*/
|
|
130
133
|
export function checkModuleManifest(
|
|
131
134
|
c2: unknown,
|
|
@@ -138,15 +141,12 @@ export function checkModuleManifest(
|
|
|
138
141
|
}
|
|
139
142
|
const summary = isObj(c2) && typeof c2.summary === "string" ? c2.summary : undefined;
|
|
140
143
|
if (summary === undefined || summary.trim() === "") {
|
|
141
|
-
return { ok: false, message: "C2 manifest summary
|
|
144
|
+
return { ok: false, message: "C2 manifest summary 缺省或空白(C2-04 必填字段)——C2-31" };
|
|
142
145
|
}
|
|
143
146
|
const desc = native.desc;
|
|
144
147
|
if (typeof desc !== "string" || desc.trim() === "") {
|
|
145
148
|
return { ok: false, message: "包内原生清单 desc 缺失或空白(oj Manifest 必填字段,serve 装配即失败)——C2-31" };
|
|
146
149
|
}
|
|
147
|
-
if (desc !== summary) {
|
|
148
|
-
return { ok: false, message: "包内原生清单 desc 与 C2 summary 不一致(构建期映射被篡改或上游不一致,拒装)——C2-31" };
|
|
149
|
-
}
|
|
150
150
|
if (native.name !== name) {
|
|
151
151
|
return { ok: false, message: `包内原生清单 name「${String(native.name)}」与安装身份「${name}」不一致——C2-31` };
|
|
152
152
|
}
|
|
@@ -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,23 +7,23 @@
|
|
|
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";
|
|
18
18
|
|
|
19
19
|
// db 为 oj isolate 注入全局(同模板 service 层惯例,installer.ts 同款;测试由夹具装 globalThis.db)
|
|
20
20
|
declare const db: {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
|
|
21
|
+
/** 安全查询构造器(与 db.query 同返回行数组) */
|
|
22
|
+
table: (name: string) => any
|
|
23
|
+
/** 目录自省(v0.1.82):当前库全部/前缀匹配的表名(方言无关;前缀 %/_ 由引擎转义) */
|
|
24
|
+
listTables: (prefix?: string) => Promise<string[]>
|
|
25
25
|
/** 单一 DB 事务包裹(P3-3):回调内语句共享一个 BEGIN/COMMIT/ROLLBACK 边界,异常即整段回滚 */
|
|
26
|
-
tx: <T>(fn: (t: {
|
|
26
|
+
tx: <T>(fn: (t: { table: (name: string) => any }) => Promise<T>) => Promise<T>
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
function isObj(v: unknown): v is Record<string, unknown> {
|
|
@@ -60,44 +60,13 @@ function manifestTables(yamlText: string): string[] {
|
|
|
60
60
|
return out;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
/**
|
|
64
|
-
function
|
|
65
|
-
|
|
66
|
-
return typeof d === "function" ? d() : "sqlite";
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** 表目录查询片段(方言无关:sqlite=sqlite_master,mysql/pg=information_schema) */
|
|
70
|
-
export function catalogSpec(dialect: string): { existing: string, like: (pat: string) => [string, unknown[]] } {
|
|
71
|
-
if (dialect === "sqlite") {
|
|
72
|
-
return {
|
|
73
|
-
existing: "SELECT name FROM sqlite_master WHERE type='table'",
|
|
74
|
-
like: pat => ["SELECT name FROM sqlite_master WHERE type='table' AND name LIKE ? ESCAPE '!'", [pat]],
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
const schemaExpr = dialect === "mysql" ? "DATABASE()" : "'public'";
|
|
78
|
-
return {
|
|
79
|
-
existing: `SELECT table_name AS name FROM information_schema.tables WHERE table_schema = ${schemaExpr}`,
|
|
80
|
-
like: pat => [`SELECT table_name AS name FROM information_schema.tables WHERE table_schema = ${schemaExpr} AND table_name LIKE ? ESCAPE '!'`, [pat]],
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** 当前库实际存在的表名集合 */
|
|
85
|
-
async function listExistingTables(dialect: string): Promise<Set<string>> {
|
|
86
|
-
const rows = (await db.query(catalogSpec(dialect).existing)) as Array<{ name: string }>;
|
|
87
|
-
return new Set(rows.map(r => r.name));
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** 模块 slug 规范化 + LIKE 转义(沿用 RBAC 权限命名空间约定:!%_ 转义,连字符→下划线作表前缀) */
|
|
91
|
-
export function tablePrefixPattern(name: string): string {
|
|
92
|
-
const slug = name.replace(/-/g, "_").replace(/[!%_]/g, c => `!${c}`);
|
|
93
|
-
// 表名用下划线分隔(非权限码的 `.`):slug_ + 通配符 %(末尾 % 不转义,保持通配语义)
|
|
94
|
-
return `${slug}_%`;
|
|
63
|
+
/** 模块 slug 规范化(连字符→下划线)作表前缀;db.listTables 内部按字面量前缀匹配(自动转义 %/_),无需手拼通配 */
|
|
64
|
+
export function moduleTablePrefix(name: string): string {
|
|
65
|
+
return `${name.replace(/-/g, "_")}_`;
|
|
95
66
|
}
|
|
96
67
|
|
|
97
68
|
/** 收集需保留的业务表(Q4):声明 + 前缀兜底;仅返回实际存在的表名 */
|
|
98
69
|
export async function collectRetainedTables(name: string, manifestPath: string): Promise<string[]> {
|
|
99
|
-
const dialect = currentDialect();
|
|
100
|
-
const existing = await listExistingTables(dialect);
|
|
101
70
|
const candidates = new Set<string>();
|
|
102
71
|
// ① 声明式权威源:manifest.tables(通知等模块已在 manifest 声明业务表归属)
|
|
103
72
|
try {
|
|
@@ -108,16 +77,17 @@ export async function collectRetainedTables(name: string, manifestPath: string):
|
|
|
108
77
|
catch {
|
|
109
78
|
// manifest 缺失(api-only 方案)→ 仅前缀兜底
|
|
110
79
|
}
|
|
111
|
-
// ②
|
|
80
|
+
// ② 前缀兜底:db.listTables(v0.1.82,方言无关,前缀转义引擎做)枚举当前库匹配模块前缀的
|
|
81
|
+
// 实际表,覆盖未声明场景。listTables 已只返回存在的表,故前缀命中的候选天然存在。
|
|
112
82
|
try {
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
for (const r of rows)
|
|
116
|
-
candidates.add(r.name);
|
|
83
|
+
for (const t of await db.listTables(moduleTablePrefix(name)))
|
|
84
|
+
candidates.add(t);
|
|
117
85
|
}
|
|
118
86
|
catch {
|
|
119
87
|
// 兜底扫描失败不阻断卸载
|
|
120
88
|
}
|
|
89
|
+
// 仅回传实际存在的表(manifest 声明的表若物理不存在则不回传)
|
|
90
|
+
const existing = new Set(await db.listTables());
|
|
121
91
|
return [...candidates].filter(t => existing.has(t));
|
|
122
92
|
}
|
|
123
93
|
|
|
@@ -155,15 +125,18 @@ export interface UninstallResult {
|
|
|
155
125
|
/** 计算卸载影响面(角色数 + 持有这些角色的用户数) */
|
|
156
126
|
export async function computeUninstallImpact(input: UninstallInput): Promise<UninstallImpact> {
|
|
157
127
|
const { name } = input;
|
|
158
|
-
const roles = await db.
|
|
128
|
+
const roles = await db.table("sys_role").select(["name"]).where({ field: "module", op: "eq", value: name }).all() as Array<{ name: string }>;
|
|
159
129
|
const roleNames = roles.map(r => r.name);
|
|
160
130
|
let affectedUsers = 0;
|
|
161
131
|
if (roleNames.length > 0) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
132
|
+
// 构造器(v0.1.82):count(distinct) 聚合一行返回,不受 LIMIT 钳制——
|
|
133
|
+
// 旧 distinct().all().length 因顶层隐式 LIMIT(默认 100)/ 显式 clamp(max_limit 默认 1000)
|
|
134
|
+
// 在受影响用户 >1000 时静默漏算(今天就有正确性风险,须尽快迁移)。
|
|
135
|
+
const rows = await db.table("sys_user_role")
|
|
136
|
+
.select([{ fn: "count", distinct: true, field: "user_id", as: "c" }])
|
|
137
|
+
.where({ field: "role_name", op: "in", value: roleNames })
|
|
138
|
+
.all() as Array<{ c: number }>;
|
|
139
|
+
affectedUsers = rows[0]?.c ?? 0;
|
|
167
140
|
}
|
|
168
141
|
return { name, affectedRoles: roleNames.length, affectedUsers };
|
|
169
142
|
}
|
|
@@ -202,7 +175,7 @@ export async function runUninstall(input: UninstallInput): Promise<UninstallResu
|
|
|
202
175
|
// 边界。任一步失败 → 整段回滚(无角色/权限/路由半残),锁不摘(fail-closed:模块仍挂载、可重试或重装自愈)。
|
|
203
176
|
try {
|
|
204
177
|
await db.tx(async (t) => {
|
|
205
|
-
await t.
|
|
178
|
+
await t.table("sys_solution_route").where({ field: "module", op: "eq", value: name }).delete().run();
|
|
206
179
|
await uninstallSolution(name, t);
|
|
207
180
|
});
|
|
208
181
|
}
|
|
@@ -213,7 +186,7 @@ export async function runUninstall(input: UninstallInput): Promise<UninstallResu
|
|
|
213
186
|
|
|
214
187
|
// ② sidecar 删条目(patch 值 null = 删除该方案键)
|
|
215
188
|
try {
|
|
216
|
-
await input.state.write({ solutions: { [name]: null } });
|
|
189
|
+
await input.state.write({ solutions: { [name]: null } } as unknown as Parameters<typeof input.state.write>[0]);
|
|
217
190
|
}
|
|
218
191
|
catch (e) {
|
|
219
192
|
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,8 +2,6 @@
|
|
|
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 "./query";
|
|
6
|
-
|
|
7
5
|
export interface ContactRow {
|
|
8
6
|
id: number
|
|
9
7
|
username: string
|
|
@@ -14,22 +12,31 @@ const DEFAULT_LIMIT = 200;
|
|
|
14
12
|
const MAX_LIMIT = 200;
|
|
15
13
|
|
|
16
14
|
export async function searchContacts(keyword: unknown, opts: { limit?: unknown } = {}): Promise<ContactRow[]> {
|
|
17
|
-
|
|
15
|
+
// 长度红线(默认 2~64,越界 400)——与 query.ts likePattern 同口径;contains 自动转义 %/_/!,无需手写 ESCAPE。
|
|
16
|
+
const kw = String(keyword ?? "").trim();
|
|
17
|
+
if (kw.length < 2 || kw.length > 64)
|
|
18
|
+
throw Object.assign(new Error(kw.length < 2 ? "keyword too short (min 2)" : "keyword too long (max 64)"), { status: 400 });
|
|
18
19
|
const rawLimit = Number.parseInt(String(opts.limit ?? ""), 10);
|
|
19
20
|
const limit = Number.isFinite(rawLimit) && rawLimit >= 1 ? Math.min(MAX_LIMIT, rawLimit) : DEFAULT_LIMIT;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
// 构造器(v0.1.81):模糊搜索走语义 op contains(自动转义 + 包 %),OR 用条件树 or,方言无关。
|
|
22
|
+
return await db.table("users")
|
|
23
|
+
.select(["id", "username", "display_name"])
|
|
24
|
+
.where({ field: "is_forbidden", op: "eq", value: 0 })
|
|
25
|
+
.where({ or: [
|
|
26
|
+
{ field: "username", op: "contains", value: kw },
|
|
27
|
+
{ field: "display_name", op: "contains", value: kw },
|
|
28
|
+
] })
|
|
29
|
+
.orderBy([{ field: "username", dir: "asc" }])
|
|
30
|
+
.limit(limit)
|
|
31
|
+
.all() as unknown as ContactRow[];
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
export async function contactDetail(id: number): Promise<Record<string, unknown>> {
|
|
29
|
-
const rows = await db.
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
const rows = await db.table("users")
|
|
36
|
+
.select(["id", "username", "display_name", "email", "avatar_base64"])
|
|
37
|
+
.where({ field: "id", op: "eq", value: id })
|
|
38
|
+
.where({ field: "is_forbidden", op: "eq", value: 0 })
|
|
39
|
+
.all();
|
|
33
40
|
if (!rows[0])
|
|
34
41
|
throw Object.assign(new Error("contact not found"), { status: 404 });
|
|
35
42
|
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 });
|