@oj-module/cli 0.1.20 → 0.2.1
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/devkit/README.md +1 -1
- package/devkit/SKILL.md +17 -3
- package/devkit/manual.md +91 -6
- package/package.json +2 -2
- package/shell-dist/assets/{index-BUyoDvUl.js → index-CLdhl3Qd.js} +61 -12
- package/shell-dist/assets/runtime.js +248 -89
- package/shell-dist/index.html +3 -3
- package/shell-dist/versions.json +3 -3
- package/src/build.ts +27 -0
- package/src/init.ts +3 -0
- package/templates/README.md +15 -0
- package/templates/api/config.yaml +14 -0
- package/templates/api/seed.mysql.sql +5 -0
- package/templates/api/seed.pg.sql +5 -0
- package/templates/api/src/_platform/_shared/respond.ts +18 -0
- package/templates/api/src/_platform/_shared/service/access.ts +99 -0
- package/templates/api/src/_platform/_shared/service/audit.ts +90 -0
- package/templates/api/src/_platform/_shared/service/contacts.ts +36 -0
- package/templates/api/src/_platform/_shared/service/guard.ts +30 -0
- package/templates/api/src/_platform/_shared/service/oplog.ts +47 -0
- package/templates/api/src/_platform/_shared/service/query.ts +53 -0
- package/templates/api/src/_platform/_shared/service/rate.ts +79 -0
- package/templates/api/src/_platform/_shared/service/roles.ts +137 -0
- package/templates/api/src/_platform/_shared/service/settings.ts +67 -0
- package/templates/api/src/_platform/_shared/service/solution-perms.ts +94 -0
- package/templates/api/src/_platform/_shared/service/users.ts +163 -0
- package/templates/api/src/_platform/contacts/api.ts +22 -0
- package/templates/api/src/_platform/contacts/detail/api.ts +18 -0
- package/templates/api/src/_platform/manifest.yaml +9 -1
- package/templates/api/src/_platform/migrations/0001__create_users.mysql.sql +9 -0
- package/templates/api/src/_platform/migrations/0001__create_users.pg.sql +9 -0
- package/templates/api/src/_platform/migrations/0002__add_users_avatar_base64.mysql.sql +3 -0
- package/templates/api/src/_platform/migrations/0003__users_source_display_name.mysql.sql +5 -0
- package/templates/api/src/_platform/migrations/0003__users_source_display_name.sql +5 -0
- package/templates/api/src/_platform/migrations/0004__sys_rbac.mysql.sql +70 -0
- package/templates/api/src/_platform/migrations/0004__sys_rbac.pg.sql +67 -0
- package/templates/api/src/_platform/migrations/0004__sys_rbac.sql +67 -0
- package/templates/api/src/_platform/migrations/0005__users_must_change.mysql.sql +3 -0
- package/templates/api/src/_platform/migrations/0005__users_must_change.sql +2 -0
- package/templates/api/src/_platform/migrations/0006__users_is_forbidden.mysql.sql +3 -0
- package/templates/api/src/_platform/migrations/0006__users_is_forbidden.sql +3 -0
- package/templates/api/src/_platform/migrations/0007__users_email.sql +3 -0
- package/templates/api/src/_platform/migrations/0008__sys_refresh_token.mysql.sql +10 -0
- package/templates/api/src/_platform/migrations/0008__sys_refresh_token.pg.sql +10 -0
- package/templates/api/src/_platform/migrations/0008__sys_refresh_token.sql +11 -0
- package/templates/api/src/_platform/migrations/0009__sys_throttle.mysql.sql +12 -0
- package/templates/api/src/_platform/migrations/0009__sys_throttle.pg.sql +12 -0
- package/templates/api/src/_platform/migrations/0009__sys_throttle.sql +13 -0
- package/templates/api/src/_platform/schema.yaml +76 -1
- package/templates/api/src/_platform/seed.mysql.sql +8 -0
- package/templates/api/src/_platform/seed.pg.sql +8 -0
- package/templates/api/src/_platform/seed.sql +19 -2
- package/templates/api/src/auth/_shared/ldap-login.ts +86 -0
- package/templates/api/src/auth/_shared/ldap.ts +14 -0
- package/templates/api/src/auth/_shared/login-throttle.ts +29 -0
- package/templates/api/src/auth/_shared/session.ts +163 -18
- package/templates/api/src/auth/api.ts +54 -44
- package/templates/api/src/auth/ldap-login/api.ts +29 -0
- package/templates/api/src/system/login-methods/api.ts +19 -0
- package/templates/api/src/system/manifest.yaml +5 -0
- package/templates/api/src/system/oplog/api.ts +53 -0
- package/templates/api/src/system/permissions/api.ts +26 -0
- package/templates/api/src/system/roles/api.ts +75 -0
- package/templates/api/src/system/roles/bindings/api.ts +29 -0
- package/templates/api/src/system/settings/api.ts +60 -0
- package/templates/api/src/system/users/api.ts +58 -0
- package/templates/api/src/system/users/forbidden/api.ts +26 -0
- package/templates/api/src/system/users/password/api.ts +26 -0
- package/templates/api/src/system/users/roles/api.ts +27 -0
- package/templates/api/src/web/user-info/api.ts +9 -18
- package/templates/api/tests/auth-ldap.spec.ts +173 -0
- package/templates/api/tests/auth-session.spec.ts +194 -0
- package/templates/api/tests/auth-throttle.spec.ts +74 -0
- package/templates/api/tests/fixture.ts +50 -0
- package/templates/api/tests/guard.spec.ts +62 -0
- package/templates/api/tests/query-utils.spec.ts +68 -0
- package/templates/api/tests/rbac-audit.spec.ts +105 -0
- package/templates/api/tests/rbac-contacts.spec.ts +85 -0
- package/templates/api/tests/rbac-guard.spec.ts +192 -0
- package/templates/api/tests/rbac-roles.spec.ts +159 -0
- package/templates/api/tests/rbac-seed.spec.ts +54 -0
- package/templates/api/tests/rbac-settings.spec.ts +79 -0
- package/templates/api/tests/rbac-solution-perms.spec.ts +168 -0
- package/templates/api/tests/rbac-tables.spec.ts +63 -0
- package/templates/api/tests/rbac-users.spec.ts +164 -0
- package/templates/api/vitest.config.ts +12 -0
- package/templates/openapi.json +1060 -31
- package/templates/pnpm-workspace.yaml +9 -0
- package/templates/web/src/auth/__client/api.schemas.ts +6 -1
- package/templates/web/src/auth/__client/api.ts +33 -1
- package/templates/web/src/login/entry.ts +17 -0
- package/templates/web/src/login/locales/en-US.json +12 -2
- package/templates/web/src/login/locales/zh-CN.json +12 -2
- package/templates/web/src/login/pages/change-password.tsx +74 -0
- package/templates/web/src/system/entry.ts +90 -0
- package/templates/web/src/system/locales/en-US.json +98 -0
- package/templates/web/src/system/locales/zh-CN.json +98 -0
- package/templates/web/src/system/pages/errors.ts +51 -0
- package/templates/web/src/system/pages/forbidden.tsx +35 -0
- package/templates/web/src/system/pages/login-methods/index.tsx +29 -0
- package/templates/web/src/system/pages/oplog/index.tsx +91 -0
- package/templates/web/src/system/pages/roles/index.tsx +91 -0
- package/templates/web/src/system/pages/settings/index.tsx +106 -0
- package/templates/web/src/system/pages/users/index.tsx +168 -0
- package/templates/web/src/system/pages/users/roles.tsx +68 -0
- package/templates/web.config.ts +8 -0
- package/vendor/host-versions.json +3 -3
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// BDD:账号管理(B6 建号加固 + B7 四接口服务层)——AC1-7~10。
|
|
2
|
+
// 红线:admin 不碰 superadmin/admin 成员资格(AC1-8);最后 superadmin 不可被撤(AC1-10);
|
|
3
|
+
// 禁止操作自己(AC1-10 注);roles 写 sys_user_role 唯一权威源 + 写时失效(AC1-7/D10);
|
|
4
|
+
// 禁用/重置口令级联删 refresh(§3.1 吊销联动)。
|
|
5
|
+
/* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
|
|
6
|
+
import type { SessionDeps } from "../src/auth/_shared/session";
|
|
7
|
+
|
|
8
|
+
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
9
|
+
import { changePasswordByAdmin, createUser, listUsers, setForbidden, updateUserRoles } from "../src/_platform/_shared/service/users";
|
|
10
|
+
import { createSession } from "../src/auth/_shared/session";
|
|
11
|
+
import { installDb } from "./fixture";
|
|
12
|
+
|
|
13
|
+
let dbo: ReturnType<typeof installDb>;
|
|
14
|
+
const g = globalThis as any;
|
|
15
|
+
|
|
16
|
+
const bcryptDeps = {
|
|
17
|
+
bcryptHash: async (pw: string) => `hashed:${pw}`,
|
|
18
|
+
bcryptVerify: async (pw: string, h: string) => h === `hashed:${pw}`,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function seedRole(name: string) {
|
|
22
|
+
dbo.prepare("INSERT OR IGNORE INTO sys_role (name, module, builtin) VALUES (?, 'framework', 1)").run(name);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function mkUser(username: string, opts: { forbidden?: boolean } = {}): number {
|
|
26
|
+
dbo.prepare("INSERT INTO users (username, password_hash, is_forbidden) VALUES (?, 'x', ?)").run(username, opts.forbidden ? 1 : 0);
|
|
27
|
+
return (dbo.prepare("SELECT id FROM users WHERE username = ?").get(username) as { id: number }).id;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function rolesOf(uid: number): string[] {
|
|
31
|
+
return (dbo.prepare("SELECT role_name FROM sys_user_role WHERE user_id = ?").all(uid) as Array<{ role_name: string }>).map(r => r.role_name);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
beforeAll(() => {
|
|
35
|
+
dbo = installDb();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
g.http = {};
|
|
40
|
+
dbo.exec("DELETE FROM sys_refresh_token");
|
|
41
|
+
dbo.exec("DELETE FROM sys_user_role");
|
|
42
|
+
dbo.exec("DELETE FROM sys_oplog");
|
|
43
|
+
dbo.exec("DELETE FROM users WHERE username LIKE 'u%'");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("B6:建号加固", () => {
|
|
47
|
+
it("拒绝 ^(ldap|sso): 前缀用户名(防抢占外来号命名空间)", async () => {
|
|
48
|
+
await expect(createUser({ username: "ldap:alice", password: "pw12345" }, bcryptDeps)).rejects.toMatchObject({ status: 400 });
|
|
49
|
+
await expect(createUser({ username: "sso:bob", password: "pw12345" }, bcryptDeps)).rejects.toMatchObject({ status: 400 });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("口令 <6 → 400;建号默认 must_change=1", async () => {
|
|
53
|
+
await expect(createUser({ username: "u_short", password: "123" }, bcryptDeps)).rejects.toMatchObject({ status: 400 });
|
|
54
|
+
const uid = await createUser({ username: "u_new", password: "pw12345" }, bcryptDeps);
|
|
55
|
+
const row = dbo.prepare("SELECT must_change, password_hash FROM users WHERE id = ?").get(uid) as { must_change: number, password_hash: string };
|
|
56
|
+
expect(Number(row.must_change)).toBe(1);
|
|
57
|
+
expect(row.password_hash).toBe("hashed:pw12345");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("重名 → 400;写审计 user.create", async () => {
|
|
61
|
+
await createUser({ username: "u_dup", password: "pw12345" }, bcryptDeps);
|
|
62
|
+
await expect(createUser({ username: "u_dup", password: "pw12345" }, bcryptDeps)).rejects.toMatchObject({ status: 400 });
|
|
63
|
+
const audit = dbo.prepare("SELECT * FROM sys_oplog WHERE action = 'user.create'").all();
|
|
64
|
+
expect(audit.length).toBe(1);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("B7/AC1-7:角色分配(sys_user_role 唯一权威源 + 写时失效)", () => {
|
|
69
|
+
it("分配/撤销写 sys_user_role;持有者下一次判权即生效(D10)", async () => {
|
|
70
|
+
seedRole("wiki.editor");
|
|
71
|
+
seedRole("viewer");
|
|
72
|
+
const uid = mkUser("u_frank");
|
|
73
|
+
await updateUserRoles(1, uid, ["wiki.editor", "viewer"]);
|
|
74
|
+
expect(rolesOf(uid).sort()).toEqual(["viewer", "wiki.editor"]);
|
|
75
|
+
await updateUserRoles(1, uid, ["viewer"]);
|
|
76
|
+
expect(rolesOf(uid)).toEqual(["viewer"]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("不存在的角色 → 400", async () => {
|
|
80
|
+
const uid = mkUser("u_norole");
|
|
81
|
+
await expect(updateUserRoles(1, uid, ["ghost.role"])).rejects.toMatchObject({ status: 400 });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("AC1-8:admin 分配 superadmin/admin 成员资格 → 403(superadmin 操作者放行)", async () => {
|
|
85
|
+
seedRole("superadmin");
|
|
86
|
+
seedRole("admin");
|
|
87
|
+
const actor = mkUser("u_sup");
|
|
88
|
+
dbo.prepare("INSERT INTO sys_user_role (user_id, role_name) VALUES (?, 'superadmin')").run(actor);
|
|
89
|
+
const uid = mkUser("u_gina");
|
|
90
|
+
await expect(updateUserRoles(1, uid, ["superadmin"])).rejects.toMatchObject({ status: 403 });
|
|
91
|
+
await expect(updateUserRoles(1, uid, ["admin"])).rejects.toMatchObject({ status: 403 });
|
|
92
|
+
await updateUserRoles(actor, uid, ["superadmin"]); // superadmin 可分配
|
|
93
|
+
expect(rolesOf(uid)).toEqual(["superadmin"]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("AC1-10:唯一 superadmin 的自撤角色/自禁被「禁止操作自己」400(服务端另有最后-superadmin 兜底)", async () => {
|
|
97
|
+
seedRole("superadmin");
|
|
98
|
+
const solo = mkUser("u_solo");
|
|
99
|
+
dbo.prepare("INSERT INTO sys_user_role (user_id, role_name) VALUES (?, 'superadmin')").run(solo);
|
|
100
|
+
await expect(updateUserRoles(solo, solo, [])).rejects.toMatchObject({ status: 400 });
|
|
101
|
+
await expect(setForbidden(solo, solo, true)).rejects.toMatchObject({ status: 400 });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("禁止操作自己:对自己改角色 → 400", async () => {
|
|
105
|
+
await expect(updateUserRoles(1, 1, ["viewer"])).rejects.toMatchObject({ status: 400 });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("审计 user.roles.update", async () => {
|
|
109
|
+
seedRole("editor");
|
|
110
|
+
const uid = mkUser("u_aud");
|
|
111
|
+
await updateUserRoles(1, uid, ["editor"]);
|
|
112
|
+
const audit = dbo.prepare("SELECT * FROM sys_oplog WHERE action = 'user.roles.update'").all() as Array<{ target: string }>;
|
|
113
|
+
expect(audit.length).toBe(1);
|
|
114
|
+
expect(audit[0].target).toBe("u_aud");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("B7:口令重置与禁用(级联吊销)", () => {
|
|
119
|
+
it("管理员重置口令:must_change=1 + 该用户 refresh 全删 + 写时失效", async () => {
|
|
120
|
+
const uid = mkUser("u_reset");
|
|
121
|
+
const s = createSession({ ...bcryptDeps, randomHex: () => "rt-1", sha256Hex: (x: string) => `h(${x})`, signAccess: () => "jwt", accessTtlSecs: 900, refreshTtlSecs: 1000 } satisfies SessionDeps);
|
|
122
|
+
dbo.prepare("UPDATE users SET password_hash = 'hashed:pw12345', must_change = 0 WHERE id = ?").run(uid);
|
|
123
|
+
await s.localLogin("u_reset", "pw12345");
|
|
124
|
+
expect(dbo.prepare("SELECT COUNT(*) c FROM sys_refresh_token WHERE user_id = ?").get(uid)).toBeDefined();
|
|
125
|
+
await changePasswordByAdmin(1, uid, "newpw123", bcryptDeps);
|
|
126
|
+
const row = dbo.prepare("SELECT must_change, password_hash FROM users WHERE id = ?").get(uid) as { must_change: number, password_hash: string };
|
|
127
|
+
expect(Number(row.must_change)).toBe(1);
|
|
128
|
+
expect(row.password_hash).toBe("hashed:newpw123");
|
|
129
|
+
const left = dbo.prepare("SELECT COUNT(*) c FROM sys_refresh_token WHERE user_id = ?").get(uid) as { c: number };
|
|
130
|
+
expect(Number(left.c)).toBe(0);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("禁止操作自己:管理员重置自己口令 → 400(自助改密走 /auth/password)", async () => {
|
|
134
|
+
await expect(changePasswordByAdmin(1, 1, "newpw123", bcryptDeps)).rejects.toMatchObject({ status: 400 });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("禁用:is_forbidden=1 + refresh 全删;禁止禁用自己", async () => {
|
|
138
|
+
const uid = mkUser("u_forbid");
|
|
139
|
+
await setForbidden(1, uid, true);
|
|
140
|
+
expect(Number((dbo.prepare("SELECT is_forbidden FROM users WHERE id = ?").get(uid) as { is_forbidden: number }).is_forbidden)).toBe(1);
|
|
141
|
+
await expect(setForbidden(1, 1, true)).rejects.toMatchObject({ status: 400 });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("审计 user.password.reset / user.forbidden.update", async () => {
|
|
145
|
+
const uid = mkUser("u_aud2");
|
|
146
|
+
await changePasswordByAdmin(1, uid, "newpw123", bcryptDeps);
|
|
147
|
+
await setForbidden(1, uid, true);
|
|
148
|
+
expect(dbo.prepare("SELECT COUNT(*) c FROM sys_oplog WHERE action = 'user.password.reset'").get()).toBeDefined();
|
|
149
|
+
expect(dbo.prepare("SELECT COUNT(*) c FROM sys_oplog WHERE action = 'user.forbidden.update'").get()).toBeDefined();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("B7:列表(用户管理页数据源)", () => {
|
|
154
|
+
it("listUsers 带角色数组,排除口令哈希", async () => {
|
|
155
|
+
seedRole("editor");
|
|
156
|
+
mkUser("u_l1");
|
|
157
|
+
const uid = mkUser("u_l2");
|
|
158
|
+
await updateUserRoles(1, uid, ["editor"]);
|
|
159
|
+
const users = await listUsers();
|
|
160
|
+
const u2 = users.find(u => u.username === "u_l2");
|
|
161
|
+
expect(u2?.roles).toEqual(["editor"]);
|
|
162
|
+
expect(JSON.stringify(users)).not.toContain("password_hash");
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { defineConfig } from "vitest/config";
|
|
3
|
+
|
|
4
|
+
// 后端模块测试(BDD):node:sqlite 内存库 + 真实迁移建表(见 tests/fixture.ts)。
|
|
5
|
+
// root 锚定本目录,保证从工程根跑 vitest 时 include 相对本目录解析。
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
root: fileURLToPath(new URL(".", import.meta.url)),
|
|
8
|
+
test: {
|
|
9
|
+
include: ["tests/**/*.spec.ts"],
|
|
10
|
+
environment: "node",
|
|
11
|
+
},
|
|
12
|
+
});
|