@oj-module/cli 0.2.5 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/devkit/manual.md +19 -17
- package/devkit/sql-builder-missing-capabilities-pr.md +234 -0
- package/package.json +2 -2
- package/shell-dist/assets/runtime.js +2 -2
- package/shell-dist/index.html +2 -2
- package/shell-dist/versions.json +3 -3
- package/src/contract/__tests__/handler-scan.test.ts +29 -0
- package/src/contract/__tests__/market-client.integration.test.ts +167 -0
- package/src/contract/handler-scan.ts +35 -4
- package/src/dev.ts +10 -2
- package/src/index.ts +3 -3
- package/src/init.ts +44 -0
- package/src/preview-config.ts +163 -0
- package/src/preview.ts +105 -56
- package/src/usage.ts +3 -2
- package/templates/api/config.yaml +4 -2
- package/templates/api/src/_platform/README.md +31 -0
- package/templates/api/src/_platform/_lifecycle/atomic.ts +1 -1
- package/templates/api/src/_platform/_lifecycle/history.ts +10 -5
- package/templates/api/src/_platform/_lifecycle/installer.ts +4 -4
- package/templates/api/src/_platform/_lifecycle/sha384.ts +1 -1
- package/templates/api/src/_platform/_lifecycle/steps/download.ts +1 -1
- package/templates/api/src/_platform/_lifecycle/steps/register.ts +57 -29
- package/templates/api/src/_platform/_lifecycle/trust.ts +1 -1
- package/templates/api/src/_platform/_lifecycle/uninstall.ts +13 -8
- package/templates/api/src/_platform/_lifecycle/verify-checks.ts +2 -2
- package/templates/api/src/_platform/_shared/service/{contacts.ts → domain/contacts.ts} +8 -6
- package/templates/api/src/_platform/_shared/service/{roles.ts → domain/roles.ts} +18 -19
- package/templates/api/src/_platform/_shared/service/{settings.ts → domain/settings.ts} +6 -6
- package/templates/api/src/_platform/_shared/service/{solution-perms.ts → domain/solution-perms.ts} +18 -17
- package/templates/api/src/_platform/_shared/service/{users.ts → domain/users.ts} +20 -26
- package/templates/api/src/_platform/_shared/service/{access.ts → infra/access.ts} +5 -8
- package/templates/api/src/_platform/_shared/service/{audit.ts → infra/audit.ts} +26 -26
- package/templates/api/src/_platform/_shared/service/{rate.ts → infra/rate.ts} +12 -12
- package/templates/api/src/_platform/_shared/site-facade.ts +1 -1
- package/templates/api/src/_platform/contacts/api.ts +2 -2
- package/templates/api/src/_platform/contacts/detail/api.ts +2 -2
- package/templates/api/src/_platform/manifest.yaml +3 -1
- package/templates/api/src/_platform/market/api.ts +54 -16
- package/templates/api/src/_platform/schema.yaml +14 -0
- package/templates/api/src/_platform/solutions/api.ts +13 -12
- package/templates/api/src/auth/_shared/ldap-login.ts +11 -7
- package/templates/api/src/auth/_shared/login-throttle.ts +2 -2
- package/templates/api/src/auth/_shared/session.ts +25 -21
- package/templates/api/src/auth/api.ts +1 -1
- package/templates/api/src/demo/todos/api.ts +1 -0
- package/templates/api/src/home/line/api.ts +4 -4
- package/templates/api/src/home/pie/api.ts +4 -4
- package/templates/api/src/notification/_shared/notify.ts +40 -38
- package/templates/api/src/notifications/api.ts +1 -4
- package/templates/api/src/personal-center/upload/api.ts +1 -1
- package/templates/api/src/system/login-methods/api.ts +1 -1
- package/templates/api/src/system/oplog/api.ts +3 -3
- package/templates/api/src/system/permissions/api.ts +2 -2
- package/templates/api/src/system/roles/api.ts +2 -2
- package/templates/api/src/system/roles/bindings/api.ts +2 -2
- package/templates/api/src/system/settings/api.ts +2 -2
- package/templates/api/src/system/users/api.ts +2 -2
- package/templates/api/src/system/users/forbidden/api.ts +2 -2
- package/templates/api/src/system/users/password/api.ts +2 -2
- package/templates/api/src/system/users/roles/api.ts +2 -2
- package/templates/api/src/web/user-info/api.ts +2 -5
- package/templates/api/tests/auth-session.spec.ts +1 -1
- package/templates/api/tests/auth-throttle.spec.ts +1 -1
- package/templates/api/tests/guard.spec.ts +1 -1
- package/templates/api/tests/lifecycle/market-api.spec.ts +1 -1
- package/templates/api/tests/lifecycle/trust.spec.ts +1 -1
- package/templates/api/tests/lifecycle/uninstall.spec.ts +1 -1
- package/templates/api/tests/query-utils.spec.ts +1 -1
- package/templates/api/tests/rbac-audit.spec.ts +1 -1
- package/templates/api/tests/rbac-contacts.spec.ts +2 -2
- package/templates/api/tests/rbac-guard.spec.ts +1 -1
- package/templates/api/tests/rbac-roles.spec.ts +2 -2
- package/templates/api/tests/rbac-settings.spec.ts +2 -2
- package/templates/api/tests/rbac-solution-perms.spec.ts +1 -1
- package/templates/api/tests/rbac-users.spec.ts +1 -1
- package/templates/api/tsconfig.json +8 -0
- package/templates/env.d.ts +2 -0
- package/templates/global.d.ts +816 -0
- package/templates/init.include.yaml +18 -1
- package/templates/logs/task-events.jsonl +0 -0
- package/templates/openapi.json +525 -13
- package/templates/package.json +2 -1
- package/templates/web/src/_platform/__client/api.schemas.ts +97 -0
- package/templates/web/src/_platform/__client/api.ts +152 -39
- package/templates/web/src/oj-market/__tests__/CardGrid.test.tsx +19 -0
- package/templates/web/src/oj-market/__tests__/InstallDialog.test.tsx +3 -0
- package/templates/web/src/oj-market/__tests__/RollbackDialog.test.tsx +1 -0
- package/templates/web/src/oj-market/__tests__/marketClient.fake.ts +1 -0
- package/templates/web/src/oj-market/api/client.ts +5 -105
- package/templates/web/src/oj-market/api/defaultClient.ts +111 -5
- package/templates/web/src/oj-market/components/CardGrid.tsx +4 -1
- package/templates/web/src/oj-market/components/CardItem.tsx +9 -5
- package/templates/web/src/oj-market/components/DetailDrawer.tsx +7 -3
- package/templates/web/src/oj-market/components/InstallDialog.tsx +1 -1
- package/templates/web/src/oj-market/hooks/useMarket.ts +4 -1
- package/templates/web/src/oj-market/locales/en-US.json +2 -1
- package/templates/web/src/oj-market/locales/zh-CN.json +2 -1
- package/templates/web/src/oj-market/pages/MarketApp.tsx +16 -1
- package/templates/web.config.ts +3 -3
- package/vendor/host-versions.json +3 -3
- /package/templates/api/src/_platform/_shared/service/{guard.ts → infra/guard.ts} +0 -0
- /package/templates/api/src/_platform/_shared/service/{oplog.ts → infra/oplog.ts} +0 -0
- /package/templates/api/src/_platform/_shared/service/{query.ts → infra/query.ts} +0 -0
|
@@ -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,6 +111,7 @@ export function createSession(deps: SessionDeps = runtimeDeps()) {
|
|
|
111
111
|
|
|
112
112
|
/** B3:刷新——轮换作旧;reuse 检测(已吊销串二次出现)全族吊销 + 审计。 */
|
|
113
113
|
async refresh(token: string): Promise<TokenBundle> {
|
|
114
|
+
// 保留 db.query:select * 无已知列清单(规则6)
|
|
114
115
|
const rows = await db.query("select * from sys_refresh_token where token_hash = ?", [hashOf(String(token ?? ""))]) as Array<Record<string, unknown>>;
|
|
115
116
|
const row = rows[0];
|
|
116
117
|
if (!row)
|
|
@@ -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
|
};
|
|
@@ -9,6 +9,7 @@ const todos = {
|
|
|
9
9
|
const where = kw ? "WHERE title LIKE ?" : "";
|
|
10
10
|
const params = kw ? [`%${kw}%`] : [];
|
|
11
11
|
try {
|
|
12
|
+
// 保留 db.query:WHERE 由 JS 字符串拼接动态生成(规则5)
|
|
12
13
|
const totalRows = await db.query(`SELECT COUNT(*) AS c FROM todos ${where}`, params);
|
|
13
14
|
const total = Number(totalRows[0]?.c ?? 0);
|
|
14
15
|
const rows = await db.query(
|
|
@@ -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) {
|
|
@@ -51,6 +51,7 @@ interface Row {
|
|
|
51
51
|
interface NotifyDb {
|
|
52
52
|
query: (sql: string, params?: unknown[]) => Promise<Row[]>
|
|
53
53
|
exec: (sql: string, params?: unknown[]) => Promise<number>
|
|
54
|
+
table: DBInstance["table"]
|
|
54
55
|
}
|
|
55
56
|
declare const db: NotifyDb;
|
|
56
57
|
|
|
@@ -83,37 +84,30 @@ function mapItem(r: Row): NoticeItem {
|
|
|
83
84
|
|
|
84
85
|
/** 给指定用户推一条按人通知,返回命名空间 id "u:<id>" */
|
|
85
86
|
export async function pushNotice(input: PushInput): Promise<string> {
|
|
86
|
-
const ins = await db.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
input.category ?? "notice",
|
|
97
|
-
],
|
|
98
|
-
);
|
|
87
|
+
const ins = await db.table("notifications").insert({
|
|
88
|
+
user_id: Number(input.userId),
|
|
89
|
+
avatar: input.avatar ?? "",
|
|
90
|
+
date: nowStr(),
|
|
91
|
+
is_read: 0,
|
|
92
|
+
message: input.message,
|
|
93
|
+
title: input.title,
|
|
94
|
+
link: input.link ?? null,
|
|
95
|
+
category: input.category ?? "notice",
|
|
96
|
+
}).returning(["id"]).run() as Array<{ id: number }>;
|
|
99
97
|
return `u:${Number(ins[0]!.id)}`;
|
|
100
98
|
}
|
|
101
99
|
|
|
102
100
|
/** 广播一条通知(全员可见,一行存储),返回命名空间 id "b:<id>" */
|
|
103
101
|
export async function pushBroadcastNotice(input: PushBroadcastInput): Promise<string> {
|
|
104
|
-
const ins = await db.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
input.category ?? "notice",
|
|
114
|
-
nowStr(),
|
|
115
|
-
],
|
|
116
|
-
);
|
|
102
|
+
const ins = await db.table("notifications_broadcast").insert({
|
|
103
|
+
avatar: input.avatar ?? "",
|
|
104
|
+
date: nowStr(),
|
|
105
|
+
message: input.message,
|
|
106
|
+
title: input.title,
|
|
107
|
+
link: input.link ?? null,
|
|
108
|
+
category: input.category ?? "notice",
|
|
109
|
+
create_time: nowStr(),
|
|
110
|
+
}).returning(["id"]).run() as Array<{ id: number }>;
|
|
117
111
|
return `b:${Number(ins[0]!.id)}`;
|
|
118
112
|
}
|
|
119
113
|
|
|
@@ -125,16 +119,18 @@ export async function delNotice(id: string, userId: string | number): Promise<bo
|
|
|
125
119
|
const [scope, raw] = splitId(id);
|
|
126
120
|
const uid = Number(userId);
|
|
127
121
|
if (scope === "b") {
|
|
128
|
-
await db.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
122
|
+
await db.table("notifications_broadcast_read")
|
|
123
|
+
.where({ field: "broadcast_id", op: "eq", value: Number(raw) })
|
|
124
|
+
.where({ field: "user_id", op: "eq", value: uid })
|
|
125
|
+
.delete()
|
|
126
|
+
.run();
|
|
132
127
|
return true;
|
|
133
128
|
}
|
|
134
|
-
const n = await db.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
129
|
+
const n = await db.table("notifications")
|
|
130
|
+
.where({ field: "id", op: "eq", value: Number(raw) })
|
|
131
|
+
.where({ field: "user_id", op: "eq", value: uid })
|
|
132
|
+
.delete()
|
|
133
|
+
.run() as number;
|
|
138
134
|
return n > 0;
|
|
139
135
|
}
|
|
140
136
|
|
|
@@ -170,13 +166,18 @@ export async function markRead(id: string, userId: string | number): Promise<boo
|
|
|
170
166
|
const uid = Number(userId);
|
|
171
167
|
const now = nowStr();
|
|
172
168
|
if (scope === "b") {
|
|
169
|
+
// 保留 db.exec:INSERT OR IGNORE 构造器无等价(引擎按方言改写 IGNORE)
|
|
173
170
|
await db.exec(
|
|
174
171
|
"INSERT OR IGNORE INTO notifications_broadcast_read (broadcast_id, user_id, read_time) VALUES (?, ?, ?)",
|
|
175
172
|
[Number(raw), uid, now],
|
|
176
173
|
);
|
|
177
174
|
return true;
|
|
178
175
|
}
|
|
179
|
-
const n = await db.
|
|
176
|
+
const n = await db.table("notifications")
|
|
177
|
+
.where({ field: "id", op: "eq", value: Number(raw) })
|
|
178
|
+
.where({ field: "user_id", op: "eq", value: uid })
|
|
179
|
+
.update({ is_read: 1 })
|
|
180
|
+
.run() as number;
|
|
180
181
|
return n > 0;
|
|
181
182
|
}
|
|
182
183
|
|
|
@@ -184,7 +185,8 @@ export async function markRead(id: string, userId: string | number): Promise<boo
|
|
|
184
185
|
export async function markAllRead(userId: string | number): Promise<void> {
|
|
185
186
|
const uid = Number(userId);
|
|
186
187
|
const now = nowStr();
|
|
187
|
-
await db.
|
|
188
|
+
await db.table("notifications").where({ field: "user_id", op: "eq", value: uid }).update({ is_read: 1 }).run();
|
|
189
|
+
// 保留 db.exec:INSERT OR IGNORE ... SELECT 构造器无法表达
|
|
188
190
|
await db.exec(
|
|
189
191
|
"INSERT OR IGNORE INTO notifications_broadcast_read (broadcast_id, user_id, read_time) SELECT id, ?, ? FROM notifications_broadcast",
|
|
190
192
|
[uid, now],
|
|
@@ -194,6 +196,6 @@ export async function markAllRead(userId: string | number): Promise<void> {
|
|
|
194
196
|
/** 清空:删按人项 + 删本人广播回执(列表消失) */
|
|
195
197
|
export async function clearNotices(userId: string | number): Promise<void> {
|
|
196
198
|
const uid = Number(userId);
|
|
197
|
-
await db.
|
|
198
|
-
await db.
|
|
199
|
+
await db.table("notifications").where({ field: "user_id", op: "eq", value: uid }).delete().run();
|
|
200
|
+
await db.table("notifications_broadcast_read").where({ field: "user_id", op: "eq", value: uid }).delete().run();
|
|
199
201
|
}
|
|
@@ -12,10 +12,7 @@
|
|
|
12
12
|
export default {
|
|
13
13
|
async get() {
|
|
14
14
|
try {
|
|
15
|
-
const rows = await db.
|
|
16
|
-
"SELECT id, avatar, date, is_read, message, title FROM notifications ORDER BY id DESC",
|
|
17
|
-
[],
|
|
18
|
-
);
|
|
15
|
+
const rows = await db.table("notifications").select(["id", "avatar", "date", "is_read", "message", "title"]).orderBy([{ field: "id", dir: "desc" }]).all();
|
|
19
16
|
json.ok(rows.map(r => ({
|
|
20
17
|
id: Number(r.id),
|
|
21
18
|
avatar: String(r.avatar),
|
|
@@ -41,7 +41,7 @@ const handler = {
|
|
|
41
41
|
const bytes = meta.key ? await blob.get(meta.key) : await http.file(0);
|
|
42
42
|
const b64 = toBase64(bytes);
|
|
43
43
|
const dataUrl = `data:${meta.content_type || "application/octet-stream"};base64,${b64}`;
|
|
44
|
-
await db.
|
|
44
|
+
await db.table("users").update({ avatar_base64: dataUrl }).where({ field: "id", op: "eq", value: Number(uid) }).run();
|
|
45
45
|
// 大文件搬运/留档用 blob(v0.1.47):搬走用 blob.move,别 get + put(字节进 V8)
|
|
46
46
|
if (meta.key)
|
|
47
47
|
await blob.del(meta.key);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { respond } from "../../_platform/_shared/respond";
|
|
2
2
|
// GET /api/system/login-methods —— 登录方式发现(C5/D2;匿名端点:登录前无 token,
|
|
3
3
|
// 项目 config.yaml auth.anonymous_paths 必须列 /system/login-methods,漏列则握手 401 登录页必坏)。
|
|
4
|
-
import { listLoginMethods } from "../../_platform/_shared/service/settings";
|
|
4
|
+
import { listLoginMethods } from "../../_platform/_shared/service/domain/settings";
|
|
5
5
|
|
|
6
6
|
async function get() {
|
|
7
7
|
await respond(async () => {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { respond } from "../../_platform/_shared/respond";
|
|
2
2
|
// GET /api/system/oplog?action=&username=&module=&limit= —— 操作日志查询(AC1-29;D13 admin 可查)。
|
|
3
3
|
// POST /api/system/oplog —— 操作日志写入(C3;登录态即可,解决方案模块记录自身操作)。
|
|
4
|
-
import { assertSystemAdmin, currentAccess } from "../../_platform/_shared/service/access";
|
|
5
|
-
import { recordOp } from "../../_platform/_shared/service/audit";
|
|
6
|
-
import { searchOplog } from "../../_platform/_shared/service/oplog";
|
|
4
|
+
import { assertSystemAdmin, currentAccess } from "../../_platform/_shared/service/infra/access";
|
|
5
|
+
import { recordOp } from "../../_platform/_shared/service/infra/audit";
|
|
6
|
+
import { searchOplog } from "../../_platform/_shared/service/infra/oplog";
|
|
7
7
|
|
|
8
8
|
async function get() {
|
|
9
9
|
await respond(async () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { respond } from "../../_platform/_shared/respond";
|
|
2
|
+
import { listPermissions } from "../../_platform/_shared/service/domain/roles";
|
|
2
3
|
// GET /api/system/permissions —— 权限点全量(C2 绑定编辑器数据源,deprecated 标灰)。
|
|
3
|
-
import { assertSystemAdmin } from "../../_platform/_shared/service/access";
|
|
4
|
-
import { listPermissions } from "../../_platform/_shared/service/roles";
|
|
4
|
+
import { assertSystemAdmin } from "../../_platform/_shared/service/infra/access";
|
|
5
5
|
|
|
6
6
|
async function get() {
|
|
7
7
|
await respond(async () => {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { respond } from "../../_platform/_shared/respond";
|
|
2
|
+
import { createRole, deleteRole, listRoles } from "../../_platform/_shared/service/domain/roles";
|
|
2
3
|
// 管理域端点 /api/system/roles(设计文档 D207:/api/system/* = 管理域,assertSystemAdmin 判权)。
|
|
3
4
|
// handler 层只编排:守卫 → service → json 信封;name 走 query(wiki 先例:静态段 + http.query)。
|
|
4
|
-
import { assertSystemAdmin } from "../../_platform/_shared/service/access";
|
|
5
|
-
import { createRole, deleteRole, listRoles } from "../../_platform/_shared/service/roles";
|
|
5
|
+
import { assertSystemAdmin } from "../../_platform/_shared/service/infra/access";
|
|
6
6
|
|
|
7
7
|
async function guard(): Promise<void> {
|
|
8
8
|
await assertSystemAdmin();
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { respond } from "../../../_platform/_shared/respond";
|
|
2
|
+
import { updateBindings } from "../../../_platform/_shared/service/domain/roles";
|
|
2
3
|
// PUT /api/system/roles/bindings?name=xxx —— 整集替换角色绑定。
|
|
3
4
|
// AC1-18 声明角色绑定可调整;AC1-19 builtin 硬拒(service 层);AC1-30 摘绑定 = 紧急吊销出口
|
|
4
5
|
// (service 内写后按持有者失效判权缓存)。
|
|
5
|
-
import { assertSystemAdmin } from "../../../_platform/_shared/service/access";
|
|
6
|
-
import { updateBindings } from "../../../_platform/_shared/service/roles";
|
|
6
|
+
import { assertSystemAdmin } from "../../../_platform/_shared/service/infra/access";
|
|
7
7
|
|
|
8
8
|
async function put() {
|
|
9
9
|
await respond(async () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { respond } from "../../_platform/_shared/respond";
|
|
2
|
+
import { listSettings, updateSetting } from "../../_platform/_shared/service/domain/settings";
|
|
2
3
|
// /api/system/settings —— 系统设置(PRD §4.3):GET 按组列出(secret 掩码),PUT 写值(写审计)。
|
|
3
|
-
import { assertSystemAdmin } from "../../_platform/_shared/service/access";
|
|
4
|
-
import { listSettings, updateSetting } from "../../_platform/_shared/service/settings";
|
|
4
|
+
import { assertSystemAdmin } from "../../_platform/_shared/service/infra/access";
|
|
5
5
|
|
|
6
6
|
const settingItemSchema = {
|
|
7
7
|
type: "object",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { respond } from "../../_platform/_shared/respond";
|
|
2
|
+
import { createUser, listUsers } from "../../_platform/_shared/service/domain/users";
|
|
2
3
|
// 管理域端点组 /api/system/users(AC1-7~10):handler 只编排——assertSystemAdmin 守卫 → service → json。
|
|
3
4
|
// 保护红线在 service 层硬 enforcement:禁止操作自己、admin 不碰成员资格、最后 superadmin 兜底。
|
|
4
5
|
// 子端点:roles/password/forbidden 各为独立目录(wiki per-endpoint 先例)。
|
|
5
|
-
import { assertSystemAdmin } from "../../_platform/_shared/service/access";
|
|
6
|
-
import { createUser, listUsers } from "../../_platform/_shared/service/users";
|
|
6
|
+
import { assertSystemAdmin } from "../../_platform/_shared/service/infra/access";
|
|
7
7
|
|
|
8
8
|
const bcryptDeps = { bcryptHash: (pw: string) => bcrypt.hash(pw) };
|
|
9
9
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { respond } from "../../../_platform/_shared/respond";
|
|
2
|
+
import { setForbidden } from "../../../_platform/_shared/service/domain/users";
|
|
2
3
|
// PUT /api/system/users/forbidden?uid= —— 禁用/解禁(禁用即删 refresh,撤销即强退)。
|
|
3
|
-
import { assertSystemAdmin } from "../../../_platform/_shared/service/access";
|
|
4
|
-
import { setForbidden } from "../../../_platform/_shared/service/users";
|
|
4
|
+
import { assertSystemAdmin } from "../../../_platform/_shared/service/infra/access";
|
|
5
5
|
|
|
6
6
|
async function put() {
|
|
7
7
|
await respond(async () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { respond } from "../../../_platform/_shared/respond";
|
|
2
|
+
import { changePasswordByAdmin } from "../../../_platform/_shared/service/domain/users";
|
|
2
3
|
// PUT /api/system/users/password?uid= —— 管理员重置口令(must_change=1 + 级联吊销 refresh)。
|
|
3
|
-
import { assertSystemAdmin } from "../../../_platform/_shared/service/access";
|
|
4
|
-
import { changePasswordByAdmin } from "../../../_platform/_shared/service/users";
|
|
4
|
+
import { assertSystemAdmin } from "../../../_platform/_shared/service/infra/access";
|
|
5
5
|
|
|
6
6
|
async function put() {
|
|
7
7
|
await respond(async () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { respond } from "../../../_platform/_shared/respond";
|
|
2
|
+
import { updateUserRoles } from "../../../_platform/_shared/service/domain/users";
|
|
2
3
|
// PUT /api/system/users/roles?uid= —— 整集替换用户角色(AC1-7;sys_user_role 权威源 + 写时失效)。
|
|
3
|
-
import { assertSystemAdmin } from "../../../_platform/_shared/service/access";
|
|
4
|
-
import { updateUserRoles } from "../../../_platform/_shared/service/users";
|
|
4
|
+
import { assertSystemAdmin } from "../../../_platform/_shared/service/infra/access";
|
|
5
5
|
|
|
6
6
|
async function put() {
|
|
7
7
|
await respond(async () => {
|
|
@@ -10,16 +10,13 @@ export default {
|
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
12
12
|
try {
|
|
13
|
-
const rows = await db.
|
|
14
|
-
"SELECT id, username, display_name, avatar_base64, email, must_change FROM users WHERE id = ?",
|
|
15
|
-
[Number(uid)],
|
|
16
|
-
);
|
|
13
|
+
const rows = await db.table("users").select(["id", "username", "display_name", "avatar_base64", "email", "must_change"]).where({ field: "id", op: "eq", value: Number(uid) }).all();
|
|
17
14
|
const row = rows[0];
|
|
18
15
|
if (!row) {
|
|
19
16
|
json.fail(404, "user not found");
|
|
20
17
|
return;
|
|
21
18
|
}
|
|
22
|
-
const roleRows = await db.
|
|
19
|
+
const roleRows = await db.table("sys_user_role").select(["role_name"]).where({ field: "user_id", op: "eq", value: Number(uid) }).all() as Array<{ role_name: string }>;
|
|
23
20
|
json.ok({
|
|
24
21
|
id: String(row.id),
|
|
25
22
|
avatar: String(row.avatar_base64 ?? ""),
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import type { SessionDeps } from "../src/auth/_shared/session";
|
|
5
5
|
|
|
6
6
|
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
7
|
-
import { assertPermission } from "../src/_platform/_shared/service/access";
|
|
7
|
+
import { assertPermission } from "../src/_platform/_shared/service/infra/access";
|
|
8
8
|
import { createSession } from "../src/auth/_shared/session";
|
|
9
9
|
import { installDb } from "./fixture";
|
|
10
10
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
/* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
|
|
6
6
|
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
7
7
|
|
|
8
|
-
import { loginBlocked, loginFailed, loginSucceeded, resetLoginThrottle } from "../src/_platform/_shared/service/rate";
|
|
8
|
+
import { loginBlocked, loginFailed, loginSucceeded, resetLoginThrottle } from "../src/_platform/_shared/service/infra/rate";
|
|
9
9
|
import { installDb } from "./fixture";
|
|
10
10
|
|
|
11
11
|
let dbo: ReturnType<typeof installDb>;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// (真实迁移建表 / 运行时全局 db 同面 / 事务往返)——S0 spike 的「一条真实判权 AC + 形态证明」。
|
|
4
4
|
import { beforeAll, describe, expect, it } from "vitest";
|
|
5
5
|
|
|
6
|
-
import { decideAccess, moduleOf } from "../src/_platform/_shared/service/guard";
|
|
6
|
+
import { decideAccess, moduleOf } from "../src/_platform/_shared/service/infra/guard";
|
|
7
7
|
import { ADMIN, installDb } from "./fixture";
|
|
8
8
|
|
|
9
9
|
const as = (roles: string[], bindings: Record<string, string[]> = {}) => ({ roles, bindings });
|
|
@@ -26,7 +26,7 @@ import { readLock, writeLock } from "../../src/_platform/_lifecycle/lock";
|
|
|
26
26
|
import { createStateStore } from "../../src/_platform/_lifecycle/state";
|
|
27
27
|
import { getTask, writeTask } from "../../src/_platform/_lifecycle/tasks";
|
|
28
28
|
import { createTrustStore } from "../../src/_platform/_lifecycle/trust";
|
|
29
|
-
import { invalidateAllUsers } from "../../src/_platform/_shared/service/access";
|
|
29
|
+
import { invalidateAllUsers } from "../../src/_platform/_shared/service/infra/access";
|
|
30
30
|
import { createMarketApi, marketEnv, parseVersionsJson, versionsJsonPathOf } from "../../src/_platform/market/api";
|
|
31
31
|
import { installDb, MARKET_HISTORY_DDL } from "../fixture";
|
|
32
32
|
|
|
@@ -11,7 +11,7 @@ import { tmpdir } from "node:os";
|
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
13
13
|
import { createTrustStore, isValidKeyId } from "../../src/_platform/_lifecycle/trust";
|
|
14
|
-
import { invalidateAllUsers } from "../../src/_platform/_shared/service/access";
|
|
14
|
+
import { invalidateAllUsers } from "../../src/_platform/_shared/service/infra/access";
|
|
15
15
|
import { createMarketApi, marketEnv } from "../../src/_platform/market/api";
|
|
16
16
|
import { installDb } from "../fixture";
|
|
17
17
|
|
|
@@ -14,7 +14,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
|
14
14
|
import { readLock, writeLock } from "../../src/_platform/_lifecycle/lock";
|
|
15
15
|
import { createStateStore } from "../../src/_platform/_lifecycle/state";
|
|
16
16
|
import { catalogSpec, collectRetainedTables, tablePrefixPattern } from "../../src/_platform/_lifecycle/uninstall";
|
|
17
|
-
import { invalidateAllUsers } from "../../src/_platform/_shared/service/access";
|
|
17
|
+
import { invalidateAllUsers } from "../../src/_platform/_shared/service/infra/access";
|
|
18
18
|
import { createMarketApi } from "../../src/_platform/market/api";
|
|
19
19
|
import { installDb, MARKET_HISTORY_DDL } from "../fixture";
|
|
20
20
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
|
|
4
4
|
import { beforeAll, describe, expect, it } from "vitest";
|
|
5
5
|
|
|
6
|
-
import { likePattern, paged } from "../src/_platform/_shared/service/query";
|
|
6
|
+
import { likePattern, paged } from "../src/_platform/_shared/service/infra/query";
|
|
7
7
|
import { installDb } from "./fixture";
|
|
8
8
|
|
|
9
9
|
let dbo: ReturnType<typeof installDb>;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
|
|
4
4
|
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
|
5
5
|
|
|
6
|
-
import { auditDeny, auditOp, recordOp, resetDenyAudit } from "../src/_platform/_shared/service/audit";
|
|
6
|
+
import { auditDeny, auditOp, recordOp, resetDenyAudit } from "../src/_platform/_shared/service/infra/audit";
|
|
7
7
|
import { installDb } from "./fixture";
|
|
8
8
|
|
|
9
9
|
let dbo: ReturnType<typeof installDb>;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
/* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
|
|
5
5
|
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
|
6
6
|
|
|
7
|
-
import { contactDetail, searchContacts } from "../src/_platform/_shared/service/contacts";
|
|
7
|
+
import { contactDetail, searchContacts } from "../src/_platform/_shared/service/domain/contacts";
|
|
8
8
|
import { installDb } from "./fixture";
|
|
9
9
|
|
|
10
10
|
let dbo: ReturnType<typeof installDb>;
|
|
@@ -74,7 +74,7 @@ describe("详情与限速", () => {
|
|
|
74
74
|
it("接口级限速:同用户突发超限 → 429", async () => {
|
|
75
75
|
const uid = mkUser("c_rate");
|
|
76
76
|
g.http = { user: { id: uid } };
|
|
77
|
-
const { rateLimit } = await import("../src/_platform/_shared/service/rate");
|
|
77
|
+
const { rateLimit } = await import("../src/_platform/_shared/service/infra/rate");
|
|
78
78
|
// 窗口 60s / 限额 3(测试参数;生产端点各自定额)
|
|
79
79
|
for (let i = 0; i < 3; i++)
|
|
80
80
|
await rateLimit(`contacts:${uid}`, 3, 60_000);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
/* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
|
|
5
5
|
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
|
6
6
|
|
|
7
|
-
import { assertPermission, assertSuperAdmin, assertSystemAdmin, invalidateUser } from "../src/_platform/_shared/service/access";
|
|
7
|
+
import { assertPermission, assertSuperAdmin, assertSystemAdmin, invalidateUser } from "../src/_platform/_shared/service/infra/access";
|
|
8
8
|
import { installDb } from "./fixture";
|
|
9
9
|
|
|
10
10
|
let dbo: ReturnType<typeof installDb>;
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
/* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
|
|
5
5
|
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { createRole, deleteRole, listPermissions, listRoles, updateBindings } from "../src/_platform/_shared/service/domain/roles";
|
|
8
|
+
import { assertPermission } from "../src/_platform/_shared/service/infra/access";
|
|
9
9
|
import { installDb } from "./fixture";
|
|
10
10
|
|
|
11
11
|
let dbo: ReturnType<typeof installDb>;
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
/* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
|
|
3
3
|
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { listLoginMethods, listSettings, updateSetting } from "../src/_platform/_shared/service/domain/settings";
|
|
6
|
+
import { searchOplog } from "../src/_platform/_shared/service/infra/oplog";
|
|
7
7
|
import { installDb } from "./fixture";
|
|
8
8
|
|
|
9
9
|
let dbo: ReturnType<typeof installDb>;
|