@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
|
@@ -27,7 +27,7 @@ import { validateTarListing } from "../_lifecycle/tgz-guard";
|
|
|
27
27
|
import { createTrustStore } from "../_lifecycle/trust";
|
|
28
28
|
import { computeUninstallImpact, runUninstall } from "../_lifecycle/uninstall";
|
|
29
29
|
import { respond } from "../_shared/respond";
|
|
30
|
-
import { assertSuperAdmin } from "../_shared/service/access";
|
|
30
|
+
import { assertSuperAdmin } from "../_shared/service/infra/access";
|
|
31
31
|
import { journalHeadOf, siteOrigin, withSiteOrigin } from "../_shared/site-facade";
|
|
32
32
|
import { BOOTSTRAP_KEY, seedBootstrapKey } from "./bootstrap";
|
|
33
33
|
|
|
@@ -182,7 +182,7 @@ async function cdnBaseOf(env: MarketEnv): Promise<string | null> {
|
|
|
182
182
|
return injected === "" ? null : injected.replace(/\/+$/, "");
|
|
183
183
|
}
|
|
184
184
|
try {
|
|
185
|
-
const rows = await db.
|
|
185
|
+
const rows = await db.table("sys_setting").select(["value", "default_value"]).where({ field: "key", op: "eq", value: "framework.market_cdn_url" }).all() as Array<Record<string, unknown>>;
|
|
186
186
|
const v = String(rows[0]?.value ?? rows[0]?.default_value ?? "").trim().replace(/\/+$/, "");
|
|
187
187
|
return v === "" ? null : v;
|
|
188
188
|
}
|
|
@@ -263,7 +263,7 @@ async function operatorOf(): Promise<string | null> {
|
|
|
263
263
|
return null;
|
|
264
264
|
}
|
|
265
265
|
try {
|
|
266
|
-
const u = await db.
|
|
266
|
+
const u = await db.table("users").select(["username"]).where({ field: "id", op: "eq", value: uid }).all() as Array<{ username: string }>;
|
|
267
267
|
return u[0]?.username ?? null;
|
|
268
268
|
}
|
|
269
269
|
catch {
|
|
@@ -274,7 +274,7 @@ async function operatorOf(): Promise<string | null> {
|
|
|
274
274
|
/** sys_solution_route enabled(reconcile route 一致性面;无行/查询失败 → null 不猜) */
|
|
275
275
|
async function routeEnabledOf(name: string): Promise<boolean | null> {
|
|
276
276
|
try {
|
|
277
|
-
const rows = await db.
|
|
277
|
+
const rows = await db.table("sys_solution_route").select(["enabled"]).where({ field: "module", op: "eq", value: name }).where({ field: "deprecated", op: "eq", value: 0 }).limit(1).all() as Array<{ enabled: unknown }>;
|
|
278
278
|
if (rows.length === 0) {
|
|
279
279
|
return null;
|
|
280
280
|
}
|
|
@@ -499,7 +499,7 @@ async function installBody(env: MarketEnv): Promise<{ task_id: string }> {
|
|
|
499
499
|
manifest,
|
|
500
500
|
name,
|
|
501
501
|
version,
|
|
502
|
-
operator: await operatorOf(),
|
|
502
|
+
operator: (await operatorOf()) ?? undefined,
|
|
503
503
|
state: await createStateStore(env.statePath),
|
|
504
504
|
archiveUrl,
|
|
505
505
|
sharedRoot: env.sharedRoot,
|
|
@@ -564,10 +564,13 @@ interface IndexRow {
|
|
|
564
564
|
/** 最近一次安装操作人(回落 market_history 行;表不存在/查询失败/无行 → null,观测面不阻断索引) */
|
|
565
565
|
async function lastInstallOperatorOf(name: string): Promise<string | null> {
|
|
566
566
|
try {
|
|
567
|
-
const rows = await db.
|
|
568
|
-
"
|
|
569
|
-
|
|
570
|
-
|
|
567
|
+
const rows = await db.table("market_history")
|
|
568
|
+
.select(["operator"])
|
|
569
|
+
.where({ field: "solution", op: "eq", value: name })
|
|
570
|
+
.where({ field: "action", op: "eq", value: "install" })
|
|
571
|
+
.orderBy([{ field: "created_at", dir: "desc" }, { field: "rowid", dir: "desc" }])
|
|
572
|
+
.limit(1)
|
|
573
|
+
.all() as Array<{ operator: unknown }>;
|
|
571
574
|
const op = rows[0]?.operator;
|
|
572
575
|
return typeof op === "string" ? op : null;
|
|
573
576
|
}
|
|
@@ -728,7 +731,7 @@ export function createMarketApi(env: MarketEnv) {
|
|
|
728
731
|
state: await createStateStore(env.statePath),
|
|
729
732
|
modulesJsonPath: env.modulesJsonPath,
|
|
730
733
|
trustPath: env.trustPath,
|
|
731
|
-
operator: await operatorOf(),
|
|
734
|
+
operator: (await operatorOf()) ?? undefined,
|
|
732
735
|
});
|
|
733
736
|
});
|
|
734
737
|
},
|
|
@@ -851,7 +854,7 @@ export function createMarketApi(env: MarketEnv) {
|
|
|
851
854
|
lockPath: `${env.sharedRoot}/manifests.yaml`,
|
|
852
855
|
state: await createStateStore(env.statePath),
|
|
853
856
|
modulesJsonPath: env.modulesJsonPath,
|
|
854
|
-
operator: await operatorOf(),
|
|
857
|
+
operator: (await operatorOf()) ?? undefined,
|
|
855
858
|
});
|
|
856
859
|
// P3-11 宿主打磨:方案已不存在 = 资源缺失语义 → 404(非 200 {ok:false});
|
|
857
860
|
// 其余领域拒绝(空锁拒启/级联失败等)维持 200 {ok:false} 人话在 message。
|
|
@@ -869,10 +872,10 @@ export function createMarketApi(env: MarketEnv) {
|
|
|
869
872
|
"history": {
|
|
870
873
|
async get() {
|
|
871
874
|
await respond(async () => {
|
|
872
|
-
const rows = (await db.
|
|
873
|
-
"
|
|
874
|
-
[]
|
|
875
|
-
|
|
875
|
+
const rows = (await db.table("market_history")
|
|
876
|
+
.select(["solution", "version", "action", "status", "operator", "message", "created_at"])
|
|
877
|
+
.orderBy([{ field: "created_at", dir: "desc" }])
|
|
878
|
+
.all()) as Array<Record<string, unknown>>;
|
|
876
879
|
return rows ?? [];
|
|
877
880
|
});
|
|
878
881
|
},
|
|
@@ -880,4 +883,39 @@ export function createMarketApi(env: MarketEnv) {
|
|
|
880
883
|
};
|
|
881
884
|
}
|
|
882
885
|
|
|
883
|
-
|
|
886
|
+
const marketApi = createMarketApi(marketEnv());
|
|
887
|
+
|
|
888
|
+
// —— 契约 I/O(schema-first,oj v0.1.72+):`.schema` 定义入参(body/query)、`.response`
|
|
889
|
+
// 定义出参,供 oj 在 handler 运行前校验 + 生成式 __client 推导有类型函数(与 notification/api.ts
|
|
890
|
+
// 同口径)。出参一律 additionalProperties:true 宽松,不约束运行期返回形状;入参对齐 handler 实际
|
|
891
|
+
// 收口(source 枚举 / 必填项),oj 先校验、handler 仍自校验(双保险,不放松运行期语义)。
|
|
892
|
+
marketApi.install.post.schema = {
|
|
893
|
+
body: {
|
|
894
|
+
type: "object",
|
|
895
|
+
required: ["source", "name"],
|
|
896
|
+
properties: { source: { type: "string", enum: ["cdn", "file"] }, name: { type: "string" }, version: { type: ["string", "null"] } },
|
|
897
|
+
},
|
|
898
|
+
};
|
|
899
|
+
marketApi.install.post.response = { type: "object", properties: { task_id: { type: "string" } }, additionalProperties: true };
|
|
900
|
+
marketApi.install.cancel.post.schema = { body: { type: "object", required: ["name"], properties: { name: { type: "string" } } } };
|
|
901
|
+
marketApi.install.cancel.post.response = { type: "object", properties: { ok: { type: "boolean" }, name: { type: "string" }, requested: { type: "boolean" } }, additionalProperties: true };
|
|
902
|
+
marketApi.task.get.schema = { query: { type: "object", required: ["id"], properties: { id: { type: "string" } } } };
|
|
903
|
+
marketApi.task.get.response = { type: "object", additionalProperties: true };
|
|
904
|
+
marketApi.enable.post.schema = { body: { type: "object", required: ["name", "enabled"], properties: { name: { type: "string" }, enabled: { type: "boolean" } } } };
|
|
905
|
+
marketApi.enable.post.response = { type: "object", additionalProperties: true };
|
|
906
|
+
marketApi.meta.get.response = { type: "object", properties: { apiVersion: { type: "string" } }, additionalProperties: true };
|
|
907
|
+
marketApi.trust.get.response = { type: "object", additionalProperties: true };
|
|
908
|
+
marketApi.trust.post.schema = { body: { type: "object", required: ["task_id"], properties: { task_id: { type: "string" } } } };
|
|
909
|
+
marketApi.trust.post.response = { type: "object", additionalProperties: true };
|
|
910
|
+
marketApi.trust.key.post.schema = { body: { type: "object", required: ["key_id", "public_key"], properties: { key_id: { type: "string" }, public_key: { type: "string" } } } };
|
|
911
|
+
marketApi.trust.key.post.response = { type: "object", additionalProperties: true };
|
|
912
|
+
marketApi.trust.key.del.schema = { query: { type: "object", required: ["key_id"], properties: { key_id: { type: "string" } } } };
|
|
913
|
+
marketApi.trust.key.del.response = { type: "object", additionalProperties: true };
|
|
914
|
+
marketApi.index.get.response = { type: "array", items: { type: "object", additionalProperties: true } };
|
|
915
|
+
marketApi["uninstall-impact"].get.schema = { query: { type: "object", required: ["name"], properties: { name: { type: "string" } } } };
|
|
916
|
+
marketApi["uninstall-impact"].get.response = { type: "object", additionalProperties: true };
|
|
917
|
+
marketApi.uninstall.post.schema = { body: { type: "object", required: ["name"], properties: { name: { type: "string" } } } };
|
|
918
|
+
marketApi.uninstall.post.response = { type: "object", additionalProperties: true };
|
|
919
|
+
marketApi.history.get.response = { type: "array", items: { type: "object", additionalProperties: true } };
|
|
920
|
+
|
|
921
|
+
export default marketApi;
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
# 框架级共有表上收 _platform 伪模块(§3-D2):归属图单源,杜绝「散在模块 seed」。
|
|
2
2
|
# 与 manifest.tables 双向一致(S005);auth 模块跨模块读 users 经 deps 声明。
|
|
3
3
|
# D10:users.roles JSON 列已移除——角色唯一权威源为 sys_user_role 关系表。
|
|
4
|
+
# _oj_migrations 为 oj 迁移 journal 内建表(oj 自建自管 DDL);此处声明只为让 db.table
|
|
5
|
+
# 元数据校验放行(journalHeadOf:深检 ledger 复核 + P3-11 预锁 guard 的真源),列与 oj 真表逐列对齐。
|
|
4
6
|
tables:
|
|
7
|
+
_oj_migrations:
|
|
8
|
+
pk: [module, version]
|
|
9
|
+
columns:
|
|
10
|
+
module: {type: text, null: false}
|
|
11
|
+
version: {type: integer, null: false}
|
|
12
|
+
name: {type: text}
|
|
13
|
+
applied_on: {type: text}
|
|
14
|
+
checksum: {type: text}
|
|
5
15
|
users:
|
|
6
16
|
pk: id
|
|
7
17
|
columns:
|
|
@@ -14,6 +24,8 @@ tables:
|
|
|
14
24
|
must_change: {type: integer, null: false}
|
|
15
25
|
is_forbidden: {type: integer, null: false}
|
|
16
26
|
email: {type: text}
|
|
27
|
+
phone_number: {type: text}
|
|
28
|
+
description: {type: text}
|
|
17
29
|
sys_setting:
|
|
18
30
|
pk: key
|
|
19
31
|
columns:
|
|
@@ -88,6 +100,20 @@ tables:
|
|
|
88
100
|
expires_at: {type: integer, null: false}
|
|
89
101
|
revoked: {type: integer, null: false}
|
|
90
102
|
created_at: {type: integer, null: false}
|
|
103
|
+
# 限速/登录节流状态表(V9__sys_throttle 经迁移创建;rate.ts 经 db.table() 访问,
|
|
104
|
+
# 须在此声明 + 在 manifest.yaml tables 登记,否则 oj 表元数据校验报 unknown table)。
|
|
105
|
+
sys_rate_window:
|
|
106
|
+
pk: rate_key
|
|
107
|
+
columns:
|
|
108
|
+
rate_key: {type: text, null: false}
|
|
109
|
+
window_start: {type: integer, null: false}
|
|
110
|
+
count: {type: integer, null: false}
|
|
111
|
+
sys_login_throttle:
|
|
112
|
+
pk: pair_key
|
|
113
|
+
columns:
|
|
114
|
+
pair_key: {type: text, null: false}
|
|
115
|
+
fail_count: {type: integer, null: false}
|
|
116
|
+
blocked_until: {type: integer, null: false}
|
|
91
117
|
# 市场操作历史(P4 上收 _platform):install/enable/disable/uninstall/upgrade/rollback 六类动作留痕。
|
|
92
118
|
# 写入由平台编排面(_lifecycle/history.ts recordHistory)落到本表;市场页只读消费。
|
|
93
119
|
market_history:
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
} from "../_lifecycle/verify-checks";
|
|
25
25
|
import { getLastReport, persistReport, verifyReportsDirOf } from "../_lifecycle/verify-report";
|
|
26
26
|
import { respond } from "../_shared/respond";
|
|
27
|
-
import { assertSystemAdmin } from "../_shared/service/access";
|
|
27
|
+
import { assertSystemAdmin } from "../_shared/service/infra/access";
|
|
28
28
|
import { journalHeadOf, siteOrigin } from "../_shared/site-facade";
|
|
29
29
|
|
|
30
30
|
/** 线格式行(与 packages/runtime/src/api/rbac/types.ts 的 SolutionRouteRow 对齐) */
|
|
@@ -82,11 +82,11 @@ async function trustItemOf(env: SolutionsEnv, name: string): Promise<VerifyItem
|
|
|
82
82
|
/** 深检主体:四项检查编排 + 可信性项 + i18n 依赖项,组装 VerifyReport{ok,checks,advise}(persist 归端点) */
|
|
83
83
|
async function verifyBody(env: SolutionsEnv, name: string): Promise<VerifyReport> {
|
|
84
84
|
// 已装校验:sys_solution_route 有该 module 行(未装 → 404,端点门之一)
|
|
85
|
-
const routeRows = await db.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
const routeRows = await db.table("sys_solution_route")
|
|
86
|
+
.select(["path", "module", "category", "order", "title_key", "required_permission", "enabled"])
|
|
87
|
+
.where({ field: "deprecated", op: "eq", value: 0 })
|
|
88
|
+
.where({ field: "module", op: "eq", value: name })
|
|
89
|
+
.all() as Array<Record<string, unknown>>;
|
|
90
90
|
if (routeRows.length === 0)
|
|
91
91
|
throw Object.assign(new Error(`方案「${name}」未安装或无路由注册`), { status: 404 });
|
|
92
92
|
|
|
@@ -97,13 +97,13 @@ async function verifyBody(env: SolutionsEnv, name: string): Promise<VerifyReport
|
|
|
97
97
|
|
|
98
98
|
// ② RBAC 完整性:声明权限点 ↔ sys_permission 实有
|
|
99
99
|
const declaredPerms = [...new Set(routes.map(r => r.required_permission).filter((p): p is string => p != null))];
|
|
100
|
-
const permRows = await db.
|
|
100
|
+
const permRows = await db.table("sys_permission").select(["code"]).all() as Array<{ code: string }>;
|
|
101
101
|
const rbac = diffPermRows(declaredPerms, new Set(permRows.map(r => r.code)));
|
|
102
102
|
|
|
103
103
|
// ③ 设置完整性:manifest 声明设置 key ↔ sys_setting 实有(永不含值)
|
|
104
104
|
const manifest = await readSolutionManifest(env, name);
|
|
105
105
|
const declaredSettings = manifest ? declaredSettingsOf(manifest.manifest) : [];
|
|
106
|
-
const settingRows = await db.
|
|
106
|
+
const settingRows = 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 }>;
|
|
107
107
|
const settings = diffSettingRows(declaredSettings, new Set(settingRows.map(r => r.key)));
|
|
108
108
|
|
|
109
109
|
// ④ 迁移账本:manifest 迁移目录最高版本 ↔ _oj_migrations 实有(journalHeadOf 复用,DRY)
|
|
@@ -167,10 +167,11 @@ export function createSolutionsApi(env: SolutionsEnv) {
|
|
|
167
167
|
return;
|
|
168
168
|
}
|
|
169
169
|
await respond(async () => {
|
|
170
|
-
const rows = await db.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
170
|
+
const rows = await db.table("sys_solution_route")
|
|
171
|
+
.select(["path", "module", "category", "order", "title_key", "required_permission", "enabled"])
|
|
172
|
+
.where({ field: "deprecated", op: "eq", value: 0 })
|
|
173
|
+
.orderBy([{ field: "module" }, { field: "order" }, { field: "path" }])
|
|
174
|
+
.all() as Array<Record<string, unknown>>;
|
|
174
175
|
return rows.map(rowOf);
|
|
175
176
|
});
|
|
176
177
|
},
|
|
@@ -25,7 +25,7 @@ function fail(status: number, message: string): never {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
async function setting(key: string): Promise<string> {
|
|
28
|
-
const rows = await db.
|
|
28
|
+
const rows = await db.table("sys_setting").select(["value"]).where({ field: "key", op: "eq", value: key }).all() as Array<{ value: string | null }>;
|
|
29
29
|
return rows[0]?.value == null ? "" : String(rows[0].value);
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -64,20 +64,24 @@ export function createLdapLogin(session: Pick<Session, "issueExternal">, gateway
|
|
|
64
64
|
// JIT find-or-create:键 "ldap:<uid>",占位 hash "!ldap" 恒 verify false(低信任来源不接管本地号);
|
|
65
65
|
// must_change=0——外来号无本地旧口令,悬挂期唯一可用写端(改密)必失败 = 号 brick(设计 §9.4)
|
|
66
66
|
const localName = `ldap:${username}`;
|
|
67
|
-
let rows = await db.
|
|
67
|
+
let rows = await db.table("users").select(["id", "is_forbidden"]).where({ field: "username", op: "eq", value: localName }).all() as Array<{ id: number, is_forbidden: number }>;
|
|
68
68
|
if (!rows.length) {
|
|
69
69
|
try {
|
|
70
|
-
await db.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
await db.table("users").insert({
|
|
71
|
+
username: localName,
|
|
72
|
+
password_hash: "!ldap",
|
|
73
|
+
source: "ldap",
|
|
74
|
+
email: firstAttr(entries[0].attrs, emailAttr) || null,
|
|
75
|
+
must_change: 0,
|
|
76
|
+
is_forbidden: 0,
|
|
77
|
+
}).run();
|
|
74
78
|
}
|
|
75
79
|
catch (e) {
|
|
76
80
|
// 唯一约束兜底(预查与插入间的竞态,同 createUser 模式):撞 username → 回查复用;其余照抛
|
|
77
81
|
if (!/unique|constraint/i.test(e instanceof Error ? e.message : String(e)))
|
|
78
82
|
throw e;
|
|
79
83
|
}
|
|
80
|
-
rows = await db.
|
|
84
|
+
rows = await db.table("users").select(["id", "is_forbidden"]).where({ field: "username", op: "eq", value: localName }).all() as Array<{ id: number, is_forbidden: number }>;
|
|
81
85
|
}
|
|
82
86
|
if (Number(rows[0].is_forbidden) === 1)
|
|
83
87
|
fail(403, "account disabled");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// 登录限速编排(B5/AC1-26):auth/login 与 auth/ldap-login 共用的 (IP, username) 双维度守卫 + 失败记账。
|
|
2
2
|
// 状态全在 DB(sys_login_throttle/sys_rate_window,迁移 0009)——oj 每请求重建模块,进程内方案恒失效(§9.3-3)。
|
|
3
|
-
import { auditOp } from "../../_platform/_shared/service/audit";
|
|
4
|
-
import { loginBlocked, loginFailed, loginSucceeded, rateLimit } from "../../_platform/_shared/service/rate";
|
|
3
|
+
import { auditOp } from "../../_platform/_shared/service/infra/audit";
|
|
4
|
+
import { loginBlocked, loginFailed, loginSucceeded, rateLimit } from "../../_platform/_shared/service/infra/rate";
|
|
5
5
|
|
|
6
6
|
/** 客户端 IP:优先 X-Forwarded-For(反代后),回落 X-Real-IP。 */
|
|
7
7
|
export async function clientIp(): Promise<string> {
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// B4:access JWT 仅载 sub(roles 走登录响应/资料接口,不进 claims)。
|
|
5
5
|
// B3:refresh = 不透明随机串 + DB 存 SHA-256 哈希(sys_refresh_token,迁移 0008);
|
|
6
6
|
// 轮换作旧 + 滑动窗口(绝对 TTL 封顶 min(旧 exp, now+30d));reuse 检测全族吊销 + 审计。
|
|
7
|
-
import { invalidateUser } from "../../_platform/_shared/service/access";
|
|
8
|
-
import { recordOp } from "../../_platform/_shared/service/audit";
|
|
7
|
+
import { invalidateUser } from "../../_platform/_shared/service/infra/access";
|
|
8
|
+
import { recordOp } from "../../_platform/_shared/service/infra/audit";
|
|
9
9
|
|
|
10
10
|
export interface SessionDeps {
|
|
11
11
|
randomHex: (bytes: number) => string
|
|
@@ -59,13 +59,10 @@ export function createSession(deps: SessionDeps = runtimeDeps()) {
|
|
|
59
59
|
const hashOf = (token: string) => deps.sha256Hex(token);
|
|
60
60
|
|
|
61
61
|
async function profileOf(uid: number): Promise<SessionUser> {
|
|
62
|
-
const rows = await db.
|
|
63
|
-
"select id, username, display_name, avatar_base64, must_change from users where id = ?",
|
|
64
|
-
[uid],
|
|
65
|
-
) as Array<Record<string, unknown>>;
|
|
62
|
+
const rows = await db.table("users").select(["id", "username", "display_name", "avatar_base64", "must_change"]).where({ field: "id", op: "eq", value: uid }).all() as Array<Record<string, unknown>>;
|
|
66
63
|
if (!rows[0])
|
|
67
64
|
fail(404, "user not found");
|
|
68
|
-
const roleRows = await db.
|
|
65
|
+
const roleRows = await db.table("sys_user_role").select(["role_name"]).where({ field: "user_id", op: "eq", value: uid }).all() as Array<{ role_name: string }>;
|
|
69
66
|
return {
|
|
70
67
|
id: uid,
|
|
71
68
|
username: String(rows[0].username),
|
|
@@ -79,10 +76,13 @@ export function createSession(deps: SessionDeps = runtimeDeps()) {
|
|
|
79
76
|
async function issueFor(uid: number): Promise<TokenBundle> {
|
|
80
77
|
const user = await profileOf(uid);
|
|
81
78
|
const refreshToken = deps.randomHex(32);
|
|
82
|
-
await db.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
79
|
+
await db.table("sys_refresh_token").insert({
|
|
80
|
+
token_hash: hashOf(refreshToken),
|
|
81
|
+
user_id: uid,
|
|
82
|
+
expires_at: now() + deps.refreshTtlSecs,
|
|
83
|
+
revoked: 0,
|
|
84
|
+
created_at: now(),
|
|
85
|
+
}).run();
|
|
86
86
|
return {
|
|
87
87
|
access_token: await deps.signAccess(String(uid)),
|
|
88
88
|
refresh_token: refreshToken,
|
|
@@ -94,13 +94,13 @@ export function createSession(deps: SessionDeps = runtimeDeps()) {
|
|
|
94
94
|
|
|
95
95
|
/** 吊销联动(§3.1):禁用/重置口令/改密 → 删该用户全部 refresh 记录。 */
|
|
96
96
|
async function revokeAll(uid: number): Promise<void> {
|
|
97
|
-
await db.
|
|
97
|
+
await db.table("sys_refresh_token").where({ field: "user_id", op: "eq", value: uid }).delete().run();
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
return {
|
|
101
101
|
/** B1+D14:本地登录——先验口令后查禁用;号不存在/口令错/禁用统一 401(不泄露账号状态)。 */
|
|
102
102
|
async localLogin(username: string, password: string): Promise<TokenBundle> {
|
|
103
|
-
const rows = await db.
|
|
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>>;
|
|
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");
|
|
@@ -111,7 +111,8 @@ export function createSession(deps: SessionDeps = runtimeDeps()) {
|
|
|
111
111
|
|
|
112
112
|
/** B3:刷新——轮换作旧;reuse 检测(已吊销串二次出现)全族吊销 + 审计。 */
|
|
113
113
|
async refresh(token: string): Promise<TokenBundle> {
|
|
114
|
-
|
|
114
|
+
// 构造器(v0.1.81):select(["*"]) 显式拉全列,无已知列清单也能用,方言无关。
|
|
115
|
+
const rows = await db.table("sys_refresh_token").select(["*"]).where({ field: "token_hash", op: "eq", value: hashOf(String(token ?? "")) }).all() as Array<Record<string, unknown>>;
|
|
115
116
|
const row = rows[0];
|
|
116
117
|
if (!row)
|
|
117
118
|
fail(401, "invalid or expired refresh token");
|
|
@@ -123,14 +124,17 @@ export function createSession(deps: SessionDeps = runtimeDeps()) {
|
|
|
123
124
|
}
|
|
124
125
|
if (Number(row.expires_at) <= now())
|
|
125
126
|
fail(401, "invalid or expired refresh token");
|
|
126
|
-
await db.
|
|
127
|
+
await db.table("sys_refresh_token").update({ revoked: 1 }).where({ field: "token_hash", op: "eq", value: row.token_hash }).run();
|
|
127
128
|
// 滑动窗口 + 绝对 30d 封顶(§3.1「绝对 TTL + 滑动」)
|
|
128
129
|
const newExp = Math.min(Number(row.expires_at), now() + deps.refreshTtlSecs);
|
|
129
130
|
const refreshToken = deps.randomHex(32);
|
|
130
|
-
await db.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
await db.table("sys_refresh_token").insert({
|
|
132
|
+
token_hash: hashOf(refreshToken),
|
|
133
|
+
user_id: uid,
|
|
134
|
+
expires_at: newExp,
|
|
135
|
+
revoked: 0,
|
|
136
|
+
created_at: now(),
|
|
137
|
+
}).run();
|
|
134
138
|
const user = await profileOf(uid);
|
|
135
139
|
return {
|
|
136
140
|
access_token: await deps.signAccess(String(uid)),
|
|
@@ -143,7 +147,7 @@ export function createSession(deps: SessionDeps = runtimeDeps()) {
|
|
|
143
147
|
|
|
144
148
|
/** AC1-27:自助改密——本人 + 旧口令校验;改密清 must_change、级联删 refresh、换发正式 token。 */
|
|
145
149
|
async changePassword(uid: number, oldPassword: string, newPassword: string): Promise<TokenBundle> {
|
|
146
|
-
const rows = await db.
|
|
150
|
+
const rows = await db.table("users").select(["password_hash"]).where({ field: "id", op: "eq", value: uid }).all() as Array<{ password_hash: string }>;
|
|
147
151
|
if (!rows[0])
|
|
148
152
|
fail(404, "user not found");
|
|
149
153
|
if (!(await deps.bcryptVerify(String(oldPassword ?? ""), String(rows[0].password_hash))))
|
|
@@ -152,8 +156,8 @@ export function createSession(deps: SessionDeps = runtimeDeps()) {
|
|
|
152
156
|
fail(400, "password too short (min 6)");
|
|
153
157
|
const hash = await deps.bcryptHash(String(newPassword));
|
|
154
158
|
await db.tx(async (t) => {
|
|
155
|
-
await t.
|
|
156
|
-
await t.
|
|
159
|
+
await t.table("users").update({ password_hash: hash, must_change: 0 }).where({ field: "id", op: "eq", value: uid }).run();
|
|
160
|
+
await t.table("sys_refresh_token").where({ field: "user_id", op: "eq", value: uid }).delete().run();
|
|
157
161
|
});
|
|
158
162
|
invalidateUser(uid);
|
|
159
163
|
return issueFor(uid);
|
|
@@ -36,7 +36,7 @@ const logout = {
|
|
|
36
36
|
// POST /api/auth/logout —— 吊销当前 refresh 串(access 到期前仍有效,语义同旧版)。
|
|
37
37
|
async post() {
|
|
38
38
|
const token = String((http.body || {}).refresh_token ?? "");
|
|
39
|
-
await db.
|
|
39
|
+
await db.table("sys_refresh_token").where({ field: "token_hash", op: "eq", value: crypto.sha256Hex(token) }).delete().run();
|
|
40
40
|
json.ok(null);
|
|
41
41
|
},
|
|
42
42
|
};
|
|
@@ -6,15 +6,15 @@
|
|
|
6
6
|
const todos = {
|
|
7
7
|
async get() {
|
|
8
8
|
const kw = http.query.keyword;
|
|
9
|
-
const
|
|
10
|
-
|
|
9
|
+
const hasKw = typeof kw === "string" && kw.length > 0;
|
|
10
|
+
// 构造器:模糊过滤走语义 op(contains 自动转义 %/_/!),count 走聚合,方言无关。
|
|
11
|
+
const build = () => db.table("todos").whereIf(hasKw, { field: "title", op: "contains", value: kw });
|
|
11
12
|
try {
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
);
|
|
13
|
+
const total = Number((await build().select([{ fn: "count", as: "c" }]).all())[0]?.c ?? 0);
|
|
14
|
+
const rows = await build()
|
|
15
|
+
.select(["id", "title", "done", "create_time", "update_time"])
|
|
16
|
+
.orderBy([{ field: "id", dir: "asc" }])
|
|
17
|
+
.all();
|
|
18
18
|
const list = rows.map(r => ({
|
|
19
19
|
id: Number(r.id),
|
|
20
20
|
title: String(r.title),
|
|
@@ -12,10 +12,10 @@ const line = {
|
|
|
12
12
|
const today = Math.floor(Date.now() / 86400000);
|
|
13
13
|
const start = today - days + 1;
|
|
14
14
|
|
|
15
|
-
const rows = await db.
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
);
|
|
15
|
+
const rows = await db.table("home_line").select([
|
|
16
|
+
"day",
|
|
17
|
+
{ fn: "sum", field: "value", as: "v" },
|
|
18
|
+
]).where({ field: "day", op: "gte", value: start }).where({ field: "day", op: "lte", value: today }).groupBy(["day"]).all();
|
|
19
19
|
const map = new Map<number, number>();
|
|
20
20
|
for (const r of rows)
|
|
21
21
|
map.set(Number(r.day), Number(r.v));
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
const pie = {
|
|
7
7
|
async get() {
|
|
8
8
|
try {
|
|
9
|
-
const rows = await db.
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
);
|
|
9
|
+
const rows = await db.table("home_pie").select([
|
|
10
|
+
"category",
|
|
11
|
+
{ fn: "sum", field: "value", as: "v" },
|
|
12
|
+
]).groupBy(["category"]).all();
|
|
13
13
|
json.ok(rows.map(r => ({ code: String(r.category), value: Number(r.v) })));
|
|
14
14
|
}
|
|
15
15
|
catch (e) {
|