@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
|
@@ -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,34 +49,37 @@ 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
|
}
|
|
73
73
|
return { deprecated };
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* 事务/非事务统一的 db 面(P3-3:可传入 db.tx 的事务句柄以纳入单一事务)。
|
|
78
|
+
* 只收 table——全构造器化后(v0.1.81 startswith 补齐最后缺口)本出口不再走裸 SQL,
|
|
79
|
+
* 契约随实现收窄:tx 句柄({table})与全局 db 均可传入。
|
|
80
|
+
*/
|
|
77
81
|
interface DbLike {
|
|
78
|
-
|
|
79
|
-
exec: (sql: string, params?: unknown[]) => Promise<unknown>
|
|
82
|
+
table: DBInstance["table"]
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
/**
|
|
@@ -88,14 +91,14 @@ interface DbLike {
|
|
|
88
91
|
* P3-3:可传入事务句柄 `d`(db.tx 的 t),使级联纳入卸载的单一 DB 事务;缺省回落全局 db(兼容既有调用面)。
|
|
89
92
|
*/
|
|
90
93
|
export async function uninstallSolution(module: string, d: DbLike = db): Promise<void> {
|
|
91
|
-
const roles = await d.
|
|
94
|
+
const roles = await d.table("sys_role").select(["name"]).where({ field: "module", op: "eq", value: module }).all() as Array<{ name: string }>;
|
|
92
95
|
for (const r of roles) {
|
|
93
|
-
await d.
|
|
94
|
-
await d.
|
|
96
|
+
await d.table("sys_role_permission").where({ field: "role_name", op: "eq", value: r.name }).delete().run();
|
|
97
|
+
await d.table("sys_user_role").where({ field: "role_name", op: "eq", value: r.name }).delete().run();
|
|
95
98
|
}
|
|
96
|
-
await d.
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
await d.
|
|
100
|
-
await d.
|
|
99
|
+
await d.table("sys_role").where({ field: "module", op: "eq", value: module }).delete().run();
|
|
100
|
+
// startswith(v0.1.81):语义化前缀匹配(⇒ `v%`),引擎自动转义 %/_/!——
|
|
101
|
+
// 替代旧「手工 escape 拼接 + 裸 SQL like ? escape '!'」(三方言 ESCAPE 同义纪律见 query.ts)
|
|
102
|
+
await d.table("sys_role_permission").where({ field: "permission_code", op: "startswith", value: `${module}.` }).delete().run();
|
|
103
|
+
await d.table("sys_permission").where({ field: "module", op: "eq", value: module }).delete().run();
|
|
101
104
|
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// 禁止操作自己(AC1-10 注);最后 superadmin 兜底(服务端二次防线);
|
|
4
4
|
// 角色唯一权威源 sys_user_role + 写时失效(AC1-7/D10);禁用/重置口令级联删 refresh(§3.1)。
|
|
5
5
|
// bcrypt 经注入(Node 侧全局不可 stub,同 session.ts 约定)。
|
|
6
|
-
import { invalidateUser } from "
|
|
7
|
-
import { recordOp } from "
|
|
6
|
+
import { invalidateUser } from "../infra/access";
|
|
7
|
+
import { recordOp } from "../infra/audit";
|
|
8
8
|
|
|
9
9
|
export interface UserDeps {
|
|
10
10
|
bcryptHash: (pw: string) => Promise<string>
|
|
@@ -28,12 +28,12 @@ function fail(status: number, message: string): never {
|
|
|
28
28
|
const RESERVED_PREFIX = /^(?:ldap|sso):/i;
|
|
29
29
|
|
|
30
30
|
async function rolesOf(uid: number): Promise<string[]> {
|
|
31
|
-
const rows = await db.
|
|
31
|
+
const rows = await db.table("sys_user_role").select(["role_name"]).where({ field: "user_id", op: "eq", value: uid }).all() as Array<{ role_name: string }>;
|
|
32
32
|
return rows.map(r => r.role_name);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
async function usernameOf(uid: number): Promise<string> {
|
|
36
|
-
const rows = await db.
|
|
36
|
+
const rows = await db.table("users").select(["username"]).where({ field: "id", op: "eq", value: uid }).all() as Array<{ username: string }>;
|
|
37
37
|
return rows[0] ? String(rows[0].username) : `#${uid}`;
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -52,10 +52,8 @@ async function assertMembershipAllowed(actorId: number, nextRoles: string[]): Pr
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
export async function listUsers(): Promise<UserRow[]> {
|
|
55
|
-
const rows = await db.
|
|
56
|
-
|
|
57
|
-
) as Array<Record<string, unknown>>;
|
|
58
|
-
const roleRows = await db.query("select user_id, role_name from sys_user_role") as Array<{ user_id: number, role_name: string }>;
|
|
55
|
+
const rows = await db.table("users").select(["id", "username", "display_name", "email", "is_forbidden", "must_change"]).orderBy([{ field: "username" }]).all() as Array<Record<string, unknown>>;
|
|
56
|
+
const roleRows = await db.table("sys_user_role").select(["user_id", "role_name"]).all() as Array<{ user_id: number, role_name: string }>;
|
|
59
57
|
const roleMap = new Map<number, string[]>();
|
|
60
58
|
for (const r of roleRows)
|
|
61
59
|
(roleMap.get(Number(r.user_id)) ?? roleMap.set(Number(r.user_id), []).get(Number(r.user_id)))!.push(r.role_name);
|
|
@@ -79,14 +77,11 @@ export async function createUser(input: { username: string, password: string, di
|
|
|
79
77
|
fail(400, "username prefix reserved for external identity sources");
|
|
80
78
|
if (!input.password || String(input.password).length < 6)
|
|
81
79
|
fail(400, "password too short (min 6)");
|
|
82
|
-
const exists = await db.
|
|
80
|
+
const exists = await db.table("users").select(["id"]).where({ field: "username", op: "eq", value: username }).all();
|
|
83
81
|
if (exists.length)
|
|
84
82
|
fail(400, "username exists");
|
|
85
83
|
const hash = await deps.bcryptHash(String(input.password));
|
|
86
|
-
const rows = await db.
|
|
87
|
-
"insert into users (username, password_hash, display_name, email, must_change) values (?, ?, ?, ?, 1) returning id",
|
|
88
|
-
[username, hash, input.displayName ?? null, input.email ?? null],
|
|
89
|
-
) as Array<{ id: number }>;
|
|
84
|
+
const rows = await db.table("users").insert({ username, password_hash: hash, display_name: input.displayName ?? null, email: input.email ?? null, must_change: 1 }).returning(["id"]).run() as Array<{ id: number }>;
|
|
90
85
|
const uid = Number(rows[0].id);
|
|
91
86
|
await recordOp({ action: "user.create", module: "framework", target: username });
|
|
92
87
|
return uid;
|
|
@@ -95,14 +90,13 @@ export async function createUser(input: { username: string, password: string, di
|
|
|
95
90
|
/** AC1-7:整集替换角色(sys_user_role 权威源),写后失效持有者判权缓存。 */
|
|
96
91
|
export async function updateUserRoles(actorId: number, targetUid: number, roles: string[]): Promise<void> {
|
|
97
92
|
assertNotSelf(actorId, targetUid);
|
|
98
|
-
const target = await db.
|
|
93
|
+
const target = await db.table("users").select(["id"]).where({ field: "id", op: "eq", value: targetUid }).all();
|
|
99
94
|
if (!target.length)
|
|
100
95
|
fail(404, "user not found");
|
|
101
96
|
const next = [...new Set(roles.map(String))];
|
|
102
97
|
if (next.length) {
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
const knownSet = new Set(known.map((r: { name: string }) => r.name));
|
|
98
|
+
const known = await db.table("sys_role").select(["name"]).where({ field: "name", op: "in", value: next }).all();
|
|
99
|
+
const knownSet = new Set(known.map(r => (r as { name: string }).name));
|
|
106
100
|
const unknown = next.filter(r => !knownSet.has(r));
|
|
107
101
|
if (unknown.length)
|
|
108
102
|
fail(400, `unknown roles: ${unknown.join(", ")}`);
|
|
@@ -112,14 +106,14 @@ export async function updateUserRoles(actorId: number, targetUid: number, roles:
|
|
|
112
106
|
const beforeSet = new Set(before);
|
|
113
107
|
// AC1-10 兜底:撤掉最后一个 superadmin 拒绝(现实可达路径已被「禁止操作自己」拦截)
|
|
114
108
|
if (beforeSet.has("superadmin") && !next.includes("superadmin")) {
|
|
115
|
-
const supers = await db.
|
|
109
|
+
const supers = await db.table("sys_user_role").select([{ fn: "count", as: "c" }]).where({ field: "role_name", op: "eq", value: "superadmin" }).all() as Array<{ c: number }>;
|
|
116
110
|
if (Number(supers[0].c) <= 1)
|
|
117
111
|
fail(400, "cannot revoke the last superadmin");
|
|
118
112
|
}
|
|
119
113
|
await db.tx(async (t) => {
|
|
120
|
-
await t.
|
|
114
|
+
await t.table("sys_user_role").where({ field: "user_id", op: "eq", value: targetUid }).delete().run();
|
|
121
115
|
for (const role of next)
|
|
122
|
-
await t.
|
|
116
|
+
await t.table("sys_user_role").insert({ user_id: targetUid, role_name: role }).run();
|
|
123
117
|
});
|
|
124
118
|
invalidateUser(Number(targetUid));
|
|
125
119
|
await recordOp({
|
|
@@ -135,13 +129,13 @@ export async function changePasswordByAdmin(actorId: number, targetUid: number,
|
|
|
135
129
|
assertNotSelf(actorId, targetUid);
|
|
136
130
|
if (!newPassword || String(newPassword).length < 6)
|
|
137
131
|
fail(400, "password too short (min 6)");
|
|
138
|
-
const rows = await db.
|
|
132
|
+
const rows = await db.table("users").select(["id"]).where({ field: "id", op: "eq", value: targetUid }).all();
|
|
139
133
|
if (!rows.length)
|
|
140
134
|
fail(404, "user not found");
|
|
141
135
|
const hash = await deps.bcryptHash(String(newPassword));
|
|
142
136
|
await db.tx(async (t) => {
|
|
143
|
-
await t.
|
|
144
|
-
await t.
|
|
137
|
+
await t.table("users").update({ password_hash: hash, must_change: 1 }).where({ field: "id", op: "eq", value: targetUid }).run();
|
|
138
|
+
await t.table("sys_refresh_token").where({ field: "user_id", op: "eq", value: targetUid }).delete().run();
|
|
145
139
|
});
|
|
146
140
|
invalidateUser(Number(targetUid));
|
|
147
141
|
await recordOp({ action: "user.password.reset", module: "framework", target: await usernameOf(Number(targetUid)) });
|
|
@@ -150,13 +144,13 @@ export async function changePasswordByAdmin(actorId: number, targetUid: number,
|
|
|
150
144
|
/** B7:禁用/解禁——禁用级联删 refresh + 写时失效。 */
|
|
151
145
|
export async function setForbidden(actorId: number, targetUid: number, forbidden: boolean): Promise<void> {
|
|
152
146
|
assertNotSelf(actorId, targetUid);
|
|
153
|
-
const rows = await db.
|
|
147
|
+
const rows = await db.table("users").select(["id"]).where({ field: "id", op: "eq", value: targetUid }).all();
|
|
154
148
|
if (!rows.length)
|
|
155
149
|
fail(404, "user not found");
|
|
156
150
|
await db.tx(async (t) => {
|
|
157
|
-
await t.
|
|
151
|
+
await t.table("users").update({ is_forbidden: forbidden ? 1 : 0 }).where({ field: "id", op: "eq", value: targetUid }).run();
|
|
158
152
|
if (forbidden)
|
|
159
|
-
await t.
|
|
153
|
+
await t.table("sys_refresh_token").where({ field: "user_id", op: "eq", value: targetUid }).delete().run();
|
|
160
154
|
});
|
|
161
155
|
invalidateUser(Number(targetUid));
|
|
162
156
|
await recordOp({ action: "user.forbidden.update", module: "framework", target: await usernameOf(Number(targetUid)), detail: { forbidden } });
|
|
@@ -20,15 +20,12 @@ interface AccessSnapshot {
|
|
|
20
20
|
const cache = new Map<number, AccessSnapshot>();
|
|
21
21
|
|
|
22
22
|
async function loadSnapshot(userId: number): Promise<AccessSnapshot> {
|
|
23
|
-
const userRows = await db.
|
|
24
|
-
const roleRows = await db.
|
|
25
|
-
const roles = roleRows.map((r
|
|
23
|
+
const userRows = await db.table("users").select(["is_forbidden", "must_change"]).where({ field: "id", op: "eq", value: userId }).all();
|
|
24
|
+
const roleRows = await db.table("sys_user_role").select(["role_name"]).where({ field: "user_id", op: "eq", value: userId }).all();
|
|
25
|
+
const roles = roleRows.map(r => (r as { role_name: string }).role_name);
|
|
26
26
|
// 只捞该用户角色的绑定(参数化 IN,三方言纪律);deprecated 不过滤(判权职责,§5)
|
|
27
27
|
const bindRows = roles.length
|
|
28
|
-
? await db.
|
|
29
|
-
`select role_name, permission_code from sys_role_permission where role_name in (${roles.map(() => "?").join(",")})`,
|
|
30
|
-
roles,
|
|
31
|
-
)
|
|
28
|
+
? await db.table("sys_role_permission").select(["role_name", "permission_code"]).where({ field: "role_name", op: "in", value: roles }).all()
|
|
32
29
|
: [];
|
|
33
30
|
const bindings: Record<string, string[]> = {};
|
|
34
31
|
for (const r of bindRows as { role_name: string, permission_code: string }[])
|
|
@@ -69,7 +66,7 @@ export async function currentAccess(): Promise<AccessSnapshot> {
|
|
|
69
66
|
const uid = http.user?.id;
|
|
70
67
|
if (uid == null)
|
|
71
68
|
deny();
|
|
72
|
-
const snap = await snapshot(uid);
|
|
69
|
+
const snap = await snapshot(Number(uid));
|
|
73
70
|
if (snap.forbidden)
|
|
74
71
|
deny();
|
|
75
72
|
if (snap.mustChange)
|
|
@@ -22,27 +22,24 @@ export interface OpEntry {
|
|
|
22
22
|
async function usernameOf(userId: number): Promise<string> {
|
|
23
23
|
if (!userId)
|
|
24
24
|
return "system";
|
|
25
|
-
const rows = await db.
|
|
25
|
+
const rows = await db.table("users").select(["username"]).where({ field: "id", op: "eq", value: userId }).all();
|
|
26
26
|
return rows[0] ? String(rows[0].username) : "unknown";
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/** 同步落表。关键路径(如 A6 角色管理)直接 await;一般路径用 auditOp。 */
|
|
30
30
|
export async function recordOp(entry: OpEntry): Promise<void> {
|
|
31
|
-
const uid = http.user?.id ?? 0;
|
|
32
|
-
await db.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
entry.level ?? 6,
|
|
44
|
-
],
|
|
45
|
-
);
|
|
31
|
+
const uid = Number(http.user?.id ?? 0);
|
|
32
|
+
await db.table("sys_oplog").insert({
|
|
33
|
+
create_time: Date.now(),
|
|
34
|
+
user_id: uid,
|
|
35
|
+
username: await usernameOf(uid),
|
|
36
|
+
action: entry.action,
|
|
37
|
+
module: entry.module,
|
|
38
|
+
version: entry.version ?? null,
|
|
39
|
+
target: entry.target ?? null,
|
|
40
|
+
detail: entry.detail == null ? null : JSON.stringify(entry.detail),
|
|
41
|
+
level: entry.level ?? 6,
|
|
42
|
+
}).run();
|
|
46
43
|
}
|
|
47
44
|
|
|
48
45
|
/** fire-and-forget:审计失败静默(写日志也失败的极端场景下,业务正确性优先)。 */
|
|
@@ -61,7 +58,7 @@ let denyQueue: Promise<void> = Promise.resolve();
|
|
|
61
58
|
* uid 在调用瞬间同步捕获——http.user 是可变全局,异步队列执行时再读会被后续请求覆盖。
|
|
62
59
|
*/
|
|
63
60
|
export function auditDeny(code: string): void {
|
|
64
|
-
const uid = http.user?.id ?? 0;
|
|
61
|
+
const uid = Number(http.user?.id ?? 0);
|
|
65
62
|
denyQueue = denyQueue.then(() => recordDeny(uid, code)).catch(() => {});
|
|
66
63
|
}
|
|
67
64
|
|
|
@@ -73,18 +70,21 @@ export function resetDenyAudit(): void {
|
|
|
73
70
|
async function recordDeny(uid: number, code: string): Promise<void> {
|
|
74
71
|
const now = Date.now();
|
|
75
72
|
// 窗口锚点 = 分钟内最近一条同 (user, code) 的 auth.deny 行;命中只更新计数,否则落首条。
|
|
76
|
-
const rows = await db.
|
|
77
|
-
"select id, detail from sys_oplog where user_id = ? and action = 'auth.deny' and target = ? and create_time > ? order by id desc limit 1",
|
|
78
|
-
[uid, code, now - DENY_WINDOW_MS],
|
|
79
|
-
);
|
|
73
|
+
const rows = await db.table("sys_oplog").select(["id", "detail"]).where({ field: "user_id", op: "eq", value: uid }).where({ field: "action", op: "eq", value: "auth.deny" }).where({ field: "target", op: "eq", value: code }).where({ field: "create_time", op: "gt", value: now - DENY_WINDOW_MS }).orderBy([{ field: "id", dir: "desc" }]).limit(1).all();
|
|
80
74
|
const hit = rows[0] as { id: number, detail: string } | undefined;
|
|
81
75
|
if (hit) {
|
|
82
76
|
const count = (JSON.parse(hit.detail) as { count: number }).count + 1;
|
|
83
|
-
await db.
|
|
77
|
+
await db.table("sys_oplog").update({ detail: JSON.stringify({ code, count }) }).where({ field: "id", op: "eq", value: hit.id }).run();
|
|
84
78
|
return;
|
|
85
79
|
}
|
|
86
|
-
await db.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
80
|
+
await db.table("sys_oplog").insert({
|
|
81
|
+
create_time: now,
|
|
82
|
+
user_id: uid,
|
|
83
|
+
username: await usernameOf(uid),
|
|
84
|
+
action: "auth.deny",
|
|
85
|
+
module: moduleOf(code),
|
|
86
|
+
target: code,
|
|
87
|
+
detail: JSON.stringify({ code, count: 1 }),
|
|
88
|
+
level: 4,
|
|
89
|
+
}).run();
|
|
90
90
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// 操作日志查询(AC1-29/D13:admin 可查;读操作不记审计的红线不适用——本表本身就是审计)。
|
|
2
|
+
// 过滤维度:action 精确 / username 包含 / module 精确;分页复用 query.ts 的 pagedFactory(构造器工厂)。
|
|
3
|
+
import { pagedFactory } from "./query";
|
|
4
|
+
|
|
5
|
+
export interface OplogRow {
|
|
6
|
+
id: number
|
|
7
|
+
create_time: number
|
|
8
|
+
user_id: number
|
|
9
|
+
username: string
|
|
10
|
+
action: string
|
|
11
|
+
module: string
|
|
12
|
+
version: string | null
|
|
13
|
+
target: string | null
|
|
14
|
+
detail: string | null
|
|
15
|
+
level: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface OplogPage {
|
|
19
|
+
list: OplogRow[]
|
|
20
|
+
total: number
|
|
21
|
+
truncated: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function searchOplog(filter: { action?: unknown, username?: unknown, module?: unknown, limit?: unknown }): Promise<OplogPage> {
|
|
25
|
+
// 构造器(v0.1.82):可选过滤走 whereIf,条件写一份(mk 工厂),count + list 复用;
|
|
26
|
+
// username 走语义 op contains(引擎转义 %/_/!,等价旧 LIKE ? ESCAPE '!' 的 %kw%)。
|
|
27
|
+
// 照抄口径:devkit scenarios 场景 42 ④。
|
|
28
|
+
// 注意 whereIf 的跳过集是 undefined/null/false/[]——**"" 是合法值会保留**(api-manual
|
|
29
|
+
// §1153),故缺失值必须归一化为 undefined(不能图省事用 "",否则会发射 `action = ''`
|
|
30
|
+
// 把结果过滤空——与旧 if (filter.x) 的「假值整条跳过」语义相悖)。
|
|
31
|
+
const action = filter.action ? String(filter.action) : undefined;
|
|
32
|
+
const module = filter.module ? String(filter.module) : undefined;
|
|
33
|
+
const username = filter.username ? String(filter.username) : undefined;
|
|
34
|
+
const mk = () => db.table("sys_oplog")
|
|
35
|
+
.whereIf(action, { field: "action", op: "eq", value: action })
|
|
36
|
+
.whereIf(module, { field: "module", op: "eq", value: module })
|
|
37
|
+
.whereIf(username, { field: "username", op: "contains", value: username });
|
|
38
|
+
const r = await pagedFactory<OplogRow>(mk, { size: filter.limit });
|
|
39
|
+
return { list: r.list, total: r.total, truncated: r.truncated };
|
|
40
|
+
}
|
|
@@ -37,6 +37,24 @@ export async function paged<T>(
|
|
|
37
37
|
return { list, total, page, size, truncated: offset + list.length < total };
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
// 构造器分页工厂(v0.1.82):条件写一份(mk 工厂),count + list 各跑一次(count 用聚合,
|
|
41
|
+
// 不受 LIMIT 钳制);替代「接受任意 SQL 片段」的旧 paged(裸 SQL 包装器)。条件收敛到构造器
|
|
42
|
+
// (contains 替代 LIKE ? ESCAPE '!'),残留任意 SQL 的场景才继续走旧 paged。
|
|
43
|
+
type Builder = ReturnType<typeof db.table>;
|
|
44
|
+
export async function pagedFactory<T>(
|
|
45
|
+
mk: () => Builder,
|
|
46
|
+
input: { page?: unknown, size?: unknown } = {},
|
|
47
|
+
): Promise<PageResult<T>> {
|
|
48
|
+
const { page, size, offset } = pageArgs(input);
|
|
49
|
+
const total = Number(
|
|
50
|
+
(await mk().select([{ fn: "count", as: "c" }]).all() as Array<Record<string, unknown>>)[0]?.c ?? 0,
|
|
51
|
+
);
|
|
52
|
+
const list = total === 0
|
|
53
|
+
? []
|
|
54
|
+
: await mk().orderBy([{ field: "id", dir: "desc" }]).limit(size).offset(offset).all() as T[];
|
|
55
|
+
return { list, total, page, size, truncated: total > page * size };
|
|
56
|
+
}
|
|
57
|
+
|
|
40
58
|
/**
|
|
41
59
|
* LIKE 模式:修剪后校验长度(默认 2~64),元字符 % _ ! 加 ! 转义,两侧包 %。
|
|
42
60
|
* SQL 侧必须写 `like ? escape '!'`(三方言同语义;sqlite 默认无转义符,mysql/pg 默认不是 !)。
|
|
@@ -12,15 +12,15 @@ export interface RateLimitOpts {
|
|
|
12
12
|
/** key 维度(如 `contacts:<uid>` / `login:ip:<ip>`):窗口内超 limit → 429。 */
|
|
13
13
|
export async function rateLimit(key: string, limit: number, windowMs: number, opts: RateLimitOpts = {}): Promise<void> {
|
|
14
14
|
const now = opts.now?.() ?? Date.now();
|
|
15
|
-
const rows = await db.
|
|
15
|
+
const rows = await db.table("sys_rate_window").select(["window_start", "count"]).where({ field: "rate_key", op: "eq", value: key }).all();
|
|
16
16
|
const w = rows[0] as { window_start: number, count: number } | undefined;
|
|
17
17
|
if (!w || now - Number(w.window_start) >= windowMs) {
|
|
18
|
-
await db.
|
|
19
|
-
await db.
|
|
18
|
+
await db.table("sys_rate_window").where({ field: "rate_key", op: "eq", value: key }).delete().run();
|
|
19
|
+
await db.table("sys_rate_window").insert({ rate_key: key, window_start: now, count: 1 }).run();
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
22
|
const count = Number(w.count) + 1;
|
|
23
|
-
await db.
|
|
23
|
+
await db.table("sys_rate_window").update({ count }).where({ field: "rate_key", op: "eq", value: key }).run();
|
|
24
24
|
if (count > limit)
|
|
25
25
|
throw Object.assign(new Error("rate limited"), { status: 429 });
|
|
26
26
|
}
|
|
@@ -44,12 +44,12 @@ export async function loginFailed(key: string, opts: LoginThrottleOpts = {}): Pr
|
|
|
44
44
|
const threshold = opts.threshold ?? 6;
|
|
45
45
|
const base = opts.baseBackoffMs ?? 15_000;
|
|
46
46
|
const now = opts.now?.() ?? Date.now();
|
|
47
|
-
const rows = await db.
|
|
48
|
-
const s = rows[0] as LoginThrottleRow | undefined;
|
|
47
|
+
const rows = await db.table("sys_login_throttle").select(["fail_count", "blocked_until"]).where({ field: "pair_key", op: "eq", value: key }).all();
|
|
48
|
+
const s = rows[0] as unknown as LoginThrottleRow | undefined;
|
|
49
49
|
const count = (s ? Number(s.fail_count) : 0) + 1;
|
|
50
50
|
const blockedUntil = count >= threshold ? now + base * 2 ** (count - threshold) : (s ? Number(s.blocked_until) : 0);
|
|
51
|
-
await db.
|
|
52
|
-
await db.
|
|
51
|
+
await db.table("sys_login_throttle").where({ field: "pair_key", op: "eq", value: key }).delete().run();
|
|
52
|
+
await db.table("sys_login_throttle").insert({ pair_key: key, fail_count: count, blocked_until: blockedUntil }).run();
|
|
53
53
|
return count;
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -58,22 +58,25 @@ export async function loginBlocked(key: string, opts: LoginThrottleOpts = {}): P
|
|
|
58
58
|
const threshold = opts.threshold ?? 6;
|
|
59
59
|
const base = opts.baseBackoffMs ?? 15_000;
|
|
60
60
|
const now = opts.now?.() ?? Date.now();
|
|
61
|
-
const rows = await db.
|
|
62
|
-
const s = rows[0] as LoginThrottleRow | undefined;
|
|
61
|
+
const rows = await db.table("sys_login_throttle").select(["fail_count", "blocked_until"]).where({ field: "pair_key", op: "eq", value: key }).all();
|
|
62
|
+
const s = rows[0] as unknown as LoginThrottleRow | undefined;
|
|
63
63
|
if (!s || Number(s.blocked_until) <= now)
|
|
64
64
|
return;
|
|
65
65
|
const count = Number(s.fail_count) + 1;
|
|
66
66
|
const blockedUntil = now + base * 2 ** (count - threshold);
|
|
67
|
-
await db.
|
|
67
|
+
await db.table("sys_login_throttle").update({ fail_count: count, blocked_until: blockedUntil }).where({ field: "pair_key", op: "eq", value: key }).run();
|
|
68
68
|
throw Object.assign(new Error("too many login attempts"), { status: 429, retryAfterMs: blockedUntil - now });
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
/** 登录成功清零连败。 */
|
|
72
72
|
export async function loginSucceeded(key: string): Promise<void> {
|
|
73
|
-
await db.
|
|
73
|
+
await db.table("sys_login_throttle").where({ field: "pair_key", op: "eq", value: key }).delete().run();
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/** 清登录限速状态(测试夹具/运维排障用)。 */
|
|
77
77
|
export async function resetLoginThrottle(): Promise<void> {
|
|
78
|
-
|
|
78
|
+
// 构造器(v0.1.82):truncate() 是唯一的「无 where 清空」写动词,命名式、可 grep,
|
|
79
|
+
// 取代裸 db.exec("delete from …")(审查上像漏写 where 的 delete)。渲染 DELETE FROM t,
|
|
80
|
+
// 租户守卫照常注入(本表非租户表=整表清空)。
|
|
81
|
+
await db.table("sys_login_throttle").truncate().run();
|
|
79
82
|
}
|
|
@@ -34,7 +34,7 @@ export function withSiteOrigin(fetch: FetchInitFace): FetchFace {
|
|
|
34
34
|
/** 站点迁移账本 head(oj journal 真源 `_oj_migrations`;表缺失/查询失败 → null = 跳过禁降版判定,不猜) */
|
|
35
35
|
export async function journalHeadOf(name: string): Promise<number | bigint | null> {
|
|
36
36
|
try {
|
|
37
|
-
const rows = await db.
|
|
37
|
+
const rows = await db.table("_oj_migrations").select([{ fn: "max", field: "version", as: "head" }]).where({ field: "module", op: "eq", value: name }).all() as Array<{ head: unknown }>;
|
|
38
38
|
const head = rows[0]?.head;
|
|
39
39
|
return head == null ? null : Number(head);
|
|
40
40
|
}
|
|
@@ -2,8 +2,8 @@ import { respond } from "../_shared/respond";
|
|
|
2
2
|
// 平台共享服务域 /api/_platform/contacts(D207:登录即可,不挂权限点,H12)。
|
|
3
3
|
// 读操作不记审计(设计 §4B);接口级限速(设计 line 199)经 rate.ts(B5 复用同源)。
|
|
4
4
|
// 单条详情走子组 /api/_platform/contacts/detail?id=(detail/api.ts,含 email/avatar)。
|
|
5
|
-
import { searchContacts } from "../_shared/service/contacts";
|
|
6
|
-
import { rateLimit } from "../_shared/service/rate";
|
|
5
|
+
import { searchContacts } from "../_shared/service/domain/contacts";
|
|
6
|
+
import { rateLimit } from "../_shared/service/infra/rate";
|
|
7
7
|
|
|
8
8
|
export default {
|
|
9
9
|
// GET /api/_platform/contacts?keyword=&limit= —— 人员目录(AC1-22~24)。
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { respond } from "../../_shared/respond";
|
|
2
2
|
// GET /api/_platform/contacts/detail?id= —— 单条详情(含 email/avatar,设计 §4B)。
|
|
3
|
-
import { contactDetail } from "../../_shared/service/contacts";
|
|
4
|
-
import { rateLimit } from "../../_shared/service/rate";
|
|
3
|
+
import { contactDetail } from "../../_shared/service/domain/contacts";
|
|
4
|
+
import { rateLimit } from "../../_shared/service/infra/rate";
|
|
5
5
|
|
|
6
6
|
export default {
|
|
7
7
|
async get() {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- L1 测试账号(仅 `oj test` 经 fixtures 灌入,不进 release 产物)。
|
|
2
|
+
--
|
|
3
|
+
-- 为什么不能用 seed 的 admin 跑 L1 登录:
|
|
4
|
+
-- ① seed 的 admin 是 must_change=1 首登改密态(D12 生产语义),且初始口令 123456
|
|
5
|
+
-- 只是开发便利——用户在 dev 库改过口令后(产品主动引导),admin 口令即漂移,
|
|
6
|
+
-- L1 若绑 admin/123456 会随机 401(本地实测踩过:dev 库口令被改成 12345678)。
|
|
7
|
+
-- ② 判权类端点(currentAccess/assertPermission)对 must_change=1 恒 403。
|
|
8
|
+
-- 故 additive 建一个专用账号:INSERT OR IGNORE 幂等,反复重放终态一致,
|
|
9
|
+
-- 不碰 admin 既有行(用户的口令/状态不受 `oj test` 影响)。
|
|
10
|
+
-- 按分号朴素切分(S006):语句内不得含分号字面量。
|
|
11
|
+
INSERT OR IGNORE INTO users (username, password_hash, avatar_base64, source, display_name, must_change, is_forbidden) VALUES ('l1tester', '$2b$10$/0DuEHsBLYp6.5ZLJCFafOOvksLbruMvs9aMhUqRSpbdY52D9RaXS', '', 'local', 'L1 测试账号', 0, 0);
|
|
12
|
+
-- 绑 superadmin:L1 若打判权类端点可直通(账号生命周期 B6 建号默认 must_change=1,这里显式 0)
|
|
13
|
+
INSERT OR IGNORE INTO sys_user_role (user_id, role_name) SELECT id, 'superadmin' FROM users WHERE username = 'l1tester';
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
name: _platform
|
|
2
|
-
desc:
|
|
2
|
+
desc: 平台内核(非业务模块、无自身业务路由前缀)。含三类:① 跨模块服务 _shared/service(infra 基础设施 + domain 管理域);② 生命周期引擎 _lifecycle(方案安装/卸载/注册/验签/信任状态机);③ 平台编排 API 面 /api/_platform/{contacts,market,solutions}(由 market/api.ts、contacts/api.ts、solutions/api.ts 暴露,全 superadmin 门禁)。运行时安装的方案包不在此树。
|
|
3
3
|
version: 0.1.0
|
|
4
4
|
tables:
|
|
5
|
+
- _oj_migrations
|
|
5
6
|
- users
|
|
6
7
|
- sys_setting
|
|
7
8
|
- sys_oplog
|
|
@@ -12,3 +13,5 @@ tables:
|
|
|
12
13
|
- sys_user_role
|
|
13
14
|
- sys_refresh_token
|
|
14
15
|
- market_history
|
|
16
|
+
- sys_rate_window
|
|
17
|
+
- sys_login_throttle
|