@oj-module/cli 0.1.21 → 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.
Files changed (107) hide show
  1. package/devkit/README.md +1 -1
  2. package/devkit/SKILL.md +17 -3
  3. package/devkit/manual.md +77 -0
  4. package/package.json +2 -2
  5. package/shell-dist/assets/{index-BUyoDvUl.js → index-CLdhl3Qd.js} +61 -12
  6. package/shell-dist/assets/runtime.js +226 -89
  7. package/shell-dist/index.html +3 -3
  8. package/shell-dist/versions.json +3 -3
  9. package/src/build.ts +25 -0
  10. package/src/init.ts +3 -0
  11. package/templates/README.md +15 -0
  12. package/templates/api/config.yaml +14 -0
  13. package/templates/api/seed.mysql.sql +5 -0
  14. package/templates/api/seed.pg.sql +5 -0
  15. package/templates/api/src/_platform/_shared/respond.ts +18 -0
  16. package/templates/api/src/_platform/_shared/service/access.ts +99 -0
  17. package/templates/api/src/_platform/_shared/service/audit.ts +90 -0
  18. package/templates/api/src/_platform/_shared/service/contacts.ts +36 -0
  19. package/templates/api/src/_platform/_shared/service/guard.ts +30 -0
  20. package/templates/api/src/_platform/_shared/service/oplog.ts +47 -0
  21. package/templates/api/src/_platform/_shared/service/query.ts +53 -0
  22. package/templates/api/src/_platform/_shared/service/rate.ts +79 -0
  23. package/templates/api/src/_platform/_shared/service/roles.ts +137 -0
  24. package/templates/api/src/_platform/_shared/service/settings.ts +67 -0
  25. package/templates/api/src/_platform/_shared/service/solution-perms.ts +94 -0
  26. package/templates/api/src/_platform/_shared/service/users.ts +163 -0
  27. package/templates/api/src/_platform/contacts/api.ts +22 -0
  28. package/templates/api/src/_platform/contacts/detail/api.ts +18 -0
  29. package/templates/api/src/_platform/manifest.yaml +9 -1
  30. package/templates/api/src/_platform/migrations/0001__create_users.mysql.sql +9 -0
  31. package/templates/api/src/_platform/migrations/0001__create_users.pg.sql +9 -0
  32. package/templates/api/src/_platform/migrations/0002__add_users_avatar_base64.mysql.sql +3 -0
  33. package/templates/api/src/_platform/migrations/0003__users_source_display_name.mysql.sql +5 -0
  34. package/templates/api/src/_platform/migrations/0003__users_source_display_name.sql +5 -0
  35. package/templates/api/src/_platform/migrations/0004__sys_rbac.mysql.sql +70 -0
  36. package/templates/api/src/_platform/migrations/0004__sys_rbac.pg.sql +67 -0
  37. package/templates/api/src/_platform/migrations/0004__sys_rbac.sql +67 -0
  38. package/templates/api/src/_platform/migrations/0005__users_must_change.mysql.sql +3 -0
  39. package/templates/api/src/_platform/migrations/0005__users_must_change.sql +2 -0
  40. package/templates/api/src/_platform/migrations/0006__users_is_forbidden.mysql.sql +3 -0
  41. package/templates/api/src/_platform/migrations/0006__users_is_forbidden.sql +3 -0
  42. package/templates/api/src/_platform/migrations/0007__users_email.sql +3 -0
  43. package/templates/api/src/_platform/migrations/0008__sys_refresh_token.mysql.sql +10 -0
  44. package/templates/api/src/_platform/migrations/0008__sys_refresh_token.pg.sql +10 -0
  45. package/templates/api/src/_platform/migrations/0008__sys_refresh_token.sql +11 -0
  46. package/templates/api/src/_platform/migrations/0009__sys_throttle.mysql.sql +12 -0
  47. package/templates/api/src/_platform/migrations/0009__sys_throttle.pg.sql +12 -0
  48. package/templates/api/src/_platform/migrations/0009__sys_throttle.sql +13 -0
  49. package/templates/api/src/_platform/schema.yaml +76 -1
  50. package/templates/api/src/_platform/seed.mysql.sql +8 -0
  51. package/templates/api/src/_platform/seed.pg.sql +8 -0
  52. package/templates/api/src/_platform/seed.sql +19 -2
  53. package/templates/api/src/auth/_shared/ldap-login.ts +86 -0
  54. package/templates/api/src/auth/_shared/ldap.ts +14 -0
  55. package/templates/api/src/auth/_shared/login-throttle.ts +29 -0
  56. package/templates/api/src/auth/_shared/session.ts +163 -18
  57. package/templates/api/src/auth/api.ts +54 -44
  58. package/templates/api/src/auth/ldap-login/api.ts +29 -0
  59. package/templates/api/src/system/login-methods/api.ts +19 -0
  60. package/templates/api/src/system/manifest.yaml +5 -0
  61. package/templates/api/src/system/oplog/api.ts +53 -0
  62. package/templates/api/src/system/permissions/api.ts +26 -0
  63. package/templates/api/src/system/roles/api.ts +75 -0
  64. package/templates/api/src/system/roles/bindings/api.ts +29 -0
  65. package/templates/api/src/system/settings/api.ts +60 -0
  66. package/templates/api/src/system/users/api.ts +58 -0
  67. package/templates/api/src/system/users/forbidden/api.ts +26 -0
  68. package/templates/api/src/system/users/password/api.ts +26 -0
  69. package/templates/api/src/system/users/roles/api.ts +27 -0
  70. package/templates/api/src/web/user-info/api.ts +9 -18
  71. package/templates/api/tests/auth-ldap.spec.ts +173 -0
  72. package/templates/api/tests/auth-session.spec.ts +194 -0
  73. package/templates/api/tests/auth-throttle.spec.ts +74 -0
  74. package/templates/api/tests/fixture.ts +50 -0
  75. package/templates/api/tests/guard.spec.ts +62 -0
  76. package/templates/api/tests/query-utils.spec.ts +68 -0
  77. package/templates/api/tests/rbac-audit.spec.ts +105 -0
  78. package/templates/api/tests/rbac-contacts.spec.ts +85 -0
  79. package/templates/api/tests/rbac-guard.spec.ts +192 -0
  80. package/templates/api/tests/rbac-roles.spec.ts +159 -0
  81. package/templates/api/tests/rbac-seed.spec.ts +54 -0
  82. package/templates/api/tests/rbac-settings.spec.ts +79 -0
  83. package/templates/api/tests/rbac-solution-perms.spec.ts +168 -0
  84. package/templates/api/tests/rbac-tables.spec.ts +63 -0
  85. package/templates/api/tests/rbac-users.spec.ts +164 -0
  86. package/templates/api/vitest.config.ts +12 -0
  87. package/templates/openapi.json +1060 -31
  88. package/templates/pnpm-workspace.yaml +9 -0
  89. package/templates/web/src/auth/__client/api.schemas.ts +6 -1
  90. package/templates/web/src/auth/__client/api.ts +33 -1
  91. package/templates/web/src/login/entry.ts +17 -0
  92. package/templates/web/src/login/locales/en-US.json +12 -2
  93. package/templates/web/src/login/locales/zh-CN.json +12 -2
  94. package/templates/web/src/login/pages/change-password.tsx +74 -0
  95. package/templates/web/src/system/entry.ts +90 -0
  96. package/templates/web/src/system/locales/en-US.json +98 -0
  97. package/templates/web/src/system/locales/zh-CN.json +98 -0
  98. package/templates/web/src/system/pages/errors.ts +51 -0
  99. package/templates/web/src/system/pages/forbidden.tsx +35 -0
  100. package/templates/web/src/system/pages/login-methods/index.tsx +29 -0
  101. package/templates/web/src/system/pages/oplog/index.tsx +91 -0
  102. package/templates/web/src/system/pages/roles/index.tsx +91 -0
  103. package/templates/web/src/system/pages/settings/index.tsx +106 -0
  104. package/templates/web/src/system/pages/users/index.tsx +168 -0
  105. package/templates/web/src/system/pages/users/roles.tsx +68 -0
  106. package/templates/web.config.ts +8 -0
  107. package/vendor/host-versions.json +3 -3
@@ -0,0 +1,67 @@
1
+ -- 框架 RBAC 六表 pg 方言覆盖(sqlite 版见 0004__sys_rbac.sql;oj-wiki 先例同型)。
2
+ -- 差异:双引号保留字与 TEXT 主键 pg 原生支持(与 sqlite 同语法);id 取 BIGSERIAL(同 0001.pg 先例)。
3
+ CREATE TABLE IF NOT EXISTS sys_setting (
4
+ key TEXT PRIMARY KEY,
5
+ module TEXT NOT NULL,
6
+ "group" TEXT NOT NULL,
7
+ type TEXT NOT NULL DEFAULT 'string',
8
+ value TEXT,
9
+ default_value TEXT,
10
+ label_key TEXT,
11
+ options TEXT,
12
+ secret INTEGER NOT NULL DEFAULT 0,
13
+ update_time INTEGER NOT NULL DEFAULT 0
14
+ );
15
+
16
+ CREATE TABLE IF NOT EXISTS sys_oplog (
17
+ id BIGSERIAL PRIMARY KEY,
18
+ create_time INTEGER NOT NULL,
19
+ user_id INTEGER NOT NULL DEFAULT 0,
20
+ username TEXT NOT NULL DEFAULT 'system',
21
+ action TEXT NOT NULL,
22
+ module TEXT NOT NULL DEFAULT 'framework',
23
+ version TEXT,
24
+ target TEXT,
25
+ detail TEXT,
26
+ level INTEGER NOT NULL DEFAULT 6
27
+ );
28
+ CREATE INDEX IF NOT EXISTS idx_sys_oplog_time ON sys_oplog (create_time, id);
29
+
30
+ CREATE TABLE IF NOT EXISTS sys_solution_route (
31
+ name TEXT NOT NULL,
32
+ version TEXT NOT NULL,
33
+ category TEXT NOT NULL,
34
+ path TEXT NOT NULL,
35
+ "order" INTEGER NOT NULL DEFAULT 0,
36
+ title_key TEXT,
37
+ enabled INTEGER NOT NULL DEFAULT 1,
38
+ PRIMARY KEY (name, version, path)
39
+ );
40
+
41
+ CREATE TABLE IF NOT EXISTS sys_role (
42
+ name TEXT PRIMARY KEY,
43
+ module TEXT NOT NULL,
44
+ title TEXT,
45
+ description TEXT,
46
+ builtin INTEGER NOT NULL DEFAULT 0,
47
+ deprecated INTEGER NOT NULL DEFAULT 0
48
+ );
49
+
50
+ CREATE TABLE IF NOT EXISTS sys_permission (
51
+ code TEXT PRIMARY KEY,
52
+ module TEXT NOT NULL,
53
+ title_key TEXT,
54
+ deprecated INTEGER NOT NULL DEFAULT 0
55
+ );
56
+
57
+ CREATE TABLE IF NOT EXISTS sys_role_permission (
58
+ role_name TEXT NOT NULL,
59
+ permission_code TEXT NOT NULL,
60
+ PRIMARY KEY (role_name, permission_code)
61
+ );
62
+
63
+ CREATE TABLE IF NOT EXISTS sys_user_role (
64
+ user_id INTEGER NOT NULL,
65
+ role_name TEXT NOT NULL,
66
+ PRIMARY KEY (user_id, role_name)
67
+ );
@@ -0,0 +1,67 @@
1
+ -- 框架 RBAC 六表(设计文档 §5.9.2;判权权威源 + 升级稳定性 deprecated 的地基)
2
+ -- sqlite 方言;mysql/pg 方言变体在阶段 A 冒烟时补齐
3
+ CREATE TABLE IF NOT EXISTS sys_setting (
4
+ key TEXT PRIMARY KEY,
5
+ module TEXT NOT NULL,
6
+ "group" TEXT NOT NULL,
7
+ type TEXT NOT NULL DEFAULT 'string',
8
+ value TEXT,
9
+ default_value TEXT,
10
+ label_key TEXT,
11
+ options TEXT,
12
+ secret INTEGER NOT NULL DEFAULT 0,
13
+ update_time INTEGER NOT NULL DEFAULT 0
14
+ );
15
+
16
+ CREATE TABLE IF NOT EXISTS sys_oplog (
17
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
18
+ create_time INTEGER NOT NULL,
19
+ user_id INTEGER NOT NULL DEFAULT 0,
20
+ username TEXT NOT NULL DEFAULT 'system',
21
+ action TEXT NOT NULL,
22
+ module TEXT NOT NULL DEFAULT 'framework',
23
+ version TEXT,
24
+ target TEXT,
25
+ detail TEXT,
26
+ level INTEGER NOT NULL DEFAULT 6
27
+ );
28
+ CREATE INDEX IF NOT EXISTS idx_sys_oplog_time ON sys_oplog (create_time, id);
29
+
30
+ CREATE TABLE IF NOT EXISTS sys_solution_route (
31
+ name TEXT NOT NULL,
32
+ version TEXT NOT NULL,
33
+ category TEXT NOT NULL,
34
+ path TEXT NOT NULL,
35
+ "order" INTEGER NOT NULL DEFAULT 0,
36
+ title_key TEXT,
37
+ enabled INTEGER NOT NULL DEFAULT 1,
38
+ PRIMARY KEY (name, version, path)
39
+ );
40
+
41
+ CREATE TABLE IF NOT EXISTS sys_role (
42
+ name TEXT PRIMARY KEY,
43
+ module TEXT NOT NULL,
44
+ title TEXT,
45
+ description TEXT,
46
+ builtin INTEGER NOT NULL DEFAULT 0,
47
+ deprecated INTEGER NOT NULL DEFAULT 0
48
+ );
49
+
50
+ CREATE TABLE IF NOT EXISTS sys_permission (
51
+ code TEXT PRIMARY KEY,
52
+ module TEXT NOT NULL,
53
+ title_key TEXT,
54
+ deprecated INTEGER NOT NULL DEFAULT 0
55
+ );
56
+
57
+ CREATE TABLE IF NOT EXISTS sys_role_permission (
58
+ role_name TEXT NOT NULL,
59
+ permission_code TEXT NOT NULL,
60
+ PRIMARY KEY (role_name, permission_code)
61
+ );
62
+
63
+ CREATE TABLE IF NOT EXISTS sys_user_role (
64
+ user_id INTEGER NOT NULL,
65
+ role_name TEXT NOT NULL,
66
+ PRIMARY KEY (user_id, role_name)
67
+ );
@@ -0,0 +1,3 @@
1
+ -- users 增 must_change 的 mysql 方言覆盖(oj-wiki 0006 先例逐字同型)。
2
+ -- 差异:整型列取 BIGINT(与 users id 同宽度口径)。
3
+ ALTER TABLE users ADD COLUMN must_change BIGINT NOT NULL DEFAULT 0;
@@ -0,0 +1,2 @@
1
+ -- 账号生命周期:must_change 首登强制改密(D12 seed 超管 / 建号默认 1,设计文档 §4A)
2
+ ALTER TABLE users ADD COLUMN must_change INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1,3 @@
1
+ -- users 增 is_forbidden 的 mysql 方言覆盖(oj-wiki 0005 先例逐字同型)。
2
+ -- 差异:整型列取 BIGINT(与 users id 同宽度口径)。
3
+ ALTER TABLE users ADD COLUMN is_forbidden BIGINT NOT NULL DEFAULT 0;
@@ -0,0 +1,3 @@
1
+ -- 禁用标记:is_forbidden=1 即拒(判权出口顺带校验,设计文档 D10/D14);登录链 B1 复用同一列
2
+ -- A3 实现期发现 A1 遗漏该列(设计文档全篇引用而迁移未落),补此迁移并回填 §9 反常识
3
+ ALTER TABLE users ADD COLUMN is_forbidden INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1,3 @@
1
+ -- 通讯录字段:email(设计 §4B 检索/详情引用而 A1 未落,A7 实现期补——is_forbidden 同先例,回填 §9)
2
+ -- 可空:本地账号无邮箱(LDAP/SSO JIT 建号回填)
3
+ ALTER TABLE users ADD COLUMN email TEXT;
@@ -0,0 +1,10 @@
1
+ -- refresh token 存储 mysql 方言覆盖(sqlite 版见 0008__sys_refresh_token.sql;oj-wiki 先例同型)。
2
+ -- 差异:整型列取 BIGINT;CREATE INDEX 无 IF NOT EXISTS(迁移账本一次应用语义兜底)。
3
+ CREATE TABLE IF NOT EXISTS sys_refresh_token (
4
+ token_hash VARCHAR(255) PRIMARY KEY,
5
+ user_id BIGINT NOT NULL,
6
+ expires_at BIGINT NOT NULL,
7
+ revoked BIGINT NOT NULL DEFAULT 0,
8
+ created_at BIGINT NOT NULL
9
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
10
+ CREATE INDEX idx_sys_refresh_token_user ON sys_refresh_token (user_id);
@@ -0,0 +1,10 @@
1
+ -- refresh token 存储 pg 方言覆盖(sqlite 版见 0008__sys_refresh_token.sql;oj-wiki 先例同型)。
2
+ -- 差异:无(TEXT 主键与 CREATE INDEX IF NOT EXISTS pg 原生支持,与 sqlite 同语法)。
3
+ CREATE TABLE IF NOT EXISTS sys_refresh_token (
4
+ token_hash TEXT PRIMARY KEY,
5
+ user_id INTEGER NOT NULL,
6
+ expires_at INTEGER NOT NULL,
7
+ revoked INTEGER NOT NULL DEFAULT 0,
8
+ created_at INTEGER NOT NULL
9
+ );
10
+ CREATE INDEX IF NOT EXISTS idx_sys_refresh_token_user ON sys_refresh_token (user_id);
@@ -0,0 +1,11 @@
1
+ -- refresh token 存储(设计 §3.1:不透明随机串,DB 存 SHA-256 哈希——A1 六表未含,B1 实现期补)。
2
+ -- 轮换作旧(revoked 标记);reuse 检测 = 已吊销串二次出现 → 按 user_id 全族吊销 + 审计;
3
+ -- is_forbidden=1 或重置口令 → 级联删该用户全部记录(§3.1 吊销联动)。
4
+ CREATE TABLE IF NOT EXISTS sys_refresh_token (
5
+ token_hash TEXT PRIMARY KEY,
6
+ user_id INTEGER NOT NULL,
7
+ expires_at INTEGER NOT NULL,
8
+ revoked INTEGER NOT NULL DEFAULT 0,
9
+ created_at INTEGER NOT NULL
10
+ );
11
+ CREATE INDEX IF NOT EXISTS idx_sys_refresh_token_user ON sys_refresh_token (user_id);
@@ -0,0 +1,12 @@
1
+ -- 限速/登录节流状态落库 mysql 方言覆盖(sqlite 版见 0009__sys_throttle.sql;oj-wiki 先例同型)。
2
+ -- 差异:整型列取 BIGINT;rate_key 避开 MySQL 保留字 key。
3
+ CREATE TABLE IF NOT EXISTS sys_rate_window (
4
+ rate_key VARCHAR(255) PRIMARY KEY,
5
+ window_start BIGINT NOT NULL,
6
+ count BIGINT NOT NULL
7
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
8
+ CREATE TABLE IF NOT EXISTS sys_login_throttle (
9
+ pair_key VARCHAR(255) PRIMARY KEY,
10
+ fail_count BIGINT NOT NULL,
11
+ blocked_until BIGINT NOT NULL DEFAULT 0
12
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@@ -0,0 +1,12 @@
1
+ -- 限速/登录节流状态落库 pg 方言覆盖(sqlite 版见 0009__sys_throttle.sql;oj-wiki 先例同型)。
2
+ -- 差异:无(TEXT 主键三方言同语法)。
3
+ CREATE TABLE IF NOT EXISTS sys_rate_window (
4
+ rate_key TEXT PRIMARY KEY,
5
+ window_start INTEGER NOT NULL,
6
+ count INTEGER NOT NULL
7
+ );
8
+ CREATE TABLE IF NOT EXISTS sys_login_throttle (
9
+ pair_key TEXT PRIMARY KEY,
10
+ fail_count INTEGER NOT NULL,
11
+ blocked_until INTEGER NOT NULL DEFAULT 0
12
+ );
@@ -0,0 +1,13 @@
1
+ -- 限速/登录节流状态落库(反常识修正:oj 运行时每请求重建模块实例,进程内 Map 不跨请求存活——
2
+ -- B5 连败阈值与接口滑动窗口在真实运行时恒失效;状态持久化后重启保留、多实例天然同源,
3
+ -- 原先「多实例换 Redis」的已知上限就此消除)。
4
+ CREATE TABLE IF NOT EXISTS sys_rate_window (
5
+ rate_key TEXT PRIMARY KEY,
6
+ window_start INTEGER NOT NULL,
7
+ count INTEGER NOT NULL
8
+ );
9
+ CREATE TABLE IF NOT EXISTS sys_login_throttle (
10
+ pair_key TEXT PRIMARY KEY,
11
+ fail_count INTEGER NOT NULL,
12
+ blocked_until INTEGER NOT NULL DEFAULT 0
13
+ );
@@ -1,5 +1,6 @@
1
1
  # 框架级共有表上收 _platform 伪模块(§3-D2):归属图单源,杜绝「散在模块 seed」。
2
2
  # 与 manifest.tables 双向一致(S005);auth 模块跨模块读 users 经 deps 声明。
3
+ # D10:users.roles JSON 列已移除——角色唯一权威源为 sys_user_role 关系表。
3
4
  tables:
4
5
  users:
5
6
  pk: id
@@ -7,5 +8,79 @@ tables:
7
8
  id: {type: integer, autoincrement: true}
8
9
  username: {type: text, null: false}
9
10
  password_hash: {type: text, null: false}
10
- roles: {type: text, null: false}
11
11
  avatar_base64: {type: text, null: false}
12
+ source: {type: text, null: false}
13
+ display_name: {type: text}
14
+ must_change: {type: integer, null: false}
15
+ is_forbidden: {type: integer, null: false}
16
+ email: {type: text}
17
+ sys_setting:
18
+ pk: key
19
+ columns:
20
+ key: {type: text, null: false}
21
+ module: {type: text, null: false}
22
+ group: {type: text, null: false}
23
+ type: {type: text, null: false}
24
+ value: {type: text}
25
+ default_value: {type: text}
26
+ label_key: {type: text}
27
+ options: {type: text}
28
+ secret: {type: integer, null: false}
29
+ update_time: {type: integer, null: false}
30
+ sys_oplog:
31
+ pk: id
32
+ columns:
33
+ id: {type: integer, autoincrement: true}
34
+ create_time: {type: integer, null: false}
35
+ user_id: {type: integer, null: false}
36
+ username: {type: text, null: false}
37
+ action: {type: text, null: false}
38
+ module: {type: text, null: false}
39
+ version: {type: text}
40
+ target: {type: text}
41
+ detail: {type: text}
42
+ level: {type: integer, null: false}
43
+ sys_solution_route:
44
+ pk: [name, version, path]
45
+ columns:
46
+ name: {type: text, null: false}
47
+ version: {type: text, null: false}
48
+ category: {type: text, null: false}
49
+ path: {type: text, null: false}
50
+ order: {type: integer, null: false}
51
+ title_key: {type: text}
52
+ enabled: {type: integer, null: false}
53
+ sys_role:
54
+ pk: name
55
+ columns:
56
+ name: {type: text, null: false}
57
+ module: {type: text, null: false}
58
+ title: {type: text}
59
+ description: {type: text}
60
+ builtin: {type: integer, null: false}
61
+ deprecated: {type: integer, null: false}
62
+ sys_permission:
63
+ pk: code
64
+ columns:
65
+ code: {type: text, null: false}
66
+ module: {type: text, null: false}
67
+ title_key: {type: text}
68
+ deprecated: {type: integer, null: false}
69
+ sys_role_permission:
70
+ pk: [role_name, permission_code]
71
+ columns:
72
+ role_name: {type: text, null: false}
73
+ permission_code: {type: text, null: false}
74
+ sys_user_role:
75
+ pk: [user_id, role_name]
76
+ columns:
77
+ user_id: {type: integer, null: false}
78
+ role_name: {type: text, null: false}
79
+ sys_refresh_token:
80
+ pk: token_hash
81
+ columns:
82
+ token_hash: {type: text, null: false}
83
+ user_id: {type: integer, null: false}
84
+ expires_at: {type: integer, null: false}
85
+ revoked: {type: integer, null: false}
86
+ created_at: {type: integer, null: false}
@@ -0,0 +1,8 @@
1
+ -- _platform 种子数据 mysql 方言覆盖(方言覆盖文件必胜;INSERT OR IGNORE → INSERT IGNORE)。
2
+ -- 语句分号切分与注释纪律同基线 seed.sql。
3
+ INSERT IGNORE INTO users (username, password_hash, must_change) VALUES ('admin', '$2y$10$ylE2NdjnYiuEcq54W/KxH.bhvGaQQg3rbmYI3mcEHX6LdQEAqixeC', 1);
4
+ INSERT IGNORE INTO sys_role (name, module, title, builtin) VALUES ('superadmin', 'framework', '超级管理员', 1);
5
+ INSERT IGNORE INTO sys_role (name, module, title, builtin) VALUES ('admin', 'framework', '系统管理员', 1);
6
+ INSERT IGNORE INTO sys_role (name, module, title, builtin) VALUES ('editor', 'framework', '编辑', 1);
7
+ INSERT IGNORE INTO sys_role (name, module, title, builtin) VALUES ('viewer', 'framework', '访客', 1);
8
+ INSERT IGNORE INTO sys_user_role (user_id, role_name) SELECT id, 'superadmin' FROM users WHERE username = 'admin';
@@ -0,0 +1,8 @@
1
+ -- _platform 种子数据 pg 方言覆盖(方言覆盖文件必胜;INSERT OR IGNORE → ON CONFLICT DO NOTHING)。
2
+ -- 语句分号切分与注释纪律同基线 seed.sql。
3
+ INSERT INTO users (username, password_hash, must_change) VALUES ('admin', '$2y$10$ylE2NdjnYiuEcq54W/KxH.bhvGaQQg3rbmYI3mcEHX6LdQEAqixeC', 1) ON CONFLICT DO NOTHING;
4
+ INSERT INTO sys_role (name, module, title, builtin) VALUES ('superadmin', 'framework', '超级管理员', 1) ON CONFLICT DO NOTHING;
5
+ INSERT INTO sys_role (name, module, title, builtin) VALUES ('admin', 'framework', '系统管理员', 1) ON CONFLICT DO NOTHING;
6
+ INSERT INTO sys_role (name, module, title, builtin) VALUES ('editor', 'framework', '编辑', 1) ON CONFLICT DO NOTHING;
7
+ INSERT INTO sys_role (name, module, title, builtin) VALUES ('viewer', 'framework', '访客', 1) ON CONFLICT DO NOTHING;
8
+ INSERT INTO sys_user_role (user_id, role_name) SELECT id, 'superadmin' FROM users WHERE username = 'admin' ON CONFLICT DO NOTHING;
@@ -1,4 +1,21 @@
1
1
  -- _platform 种子数据(只放数据,DDL 归 migrations/,S006 纪律)。
2
+ -- 方言覆盖:seed.mysql.sql / seed.pg.sql(INSERT OR IGNORE 为 sqlite 专属语法,方言文件必胜,同迁移约定)。
2
3
  -- 注意:seed 按英文分号朴素切分语句,注释里也不要出现英文分号。
3
- -- admin / 123456(bcrypt)
4
- INSERT OR IGNORE INTO users (username, password_hash, roles) VALUES ('admin', '$2y$10$ylE2NdjnYiuEcq54W/KxH.bhvGaQQg3rbmYI3mcEHX6LdQEAqixeC', '["admin"]')
4
+ -- 账号生命周期 D12:admin 为 seed 超管(superadmin),must_change=1 首登强制改密——
5
+ -- 初始口令 123456 仅为开发便利,must_change 闭环兜底默认凭据窗口(口令不随机生成的取舍见计划 A2 阶段小结)
6
+ INSERT OR IGNORE INTO users (username, password_hash, must_change) VALUES ('admin', '$2y$10$ylE2NdjnYiuEcq54W/KxH.bhvGaQQg3rbmYI3mcEHX6LdQEAqixeC', 1);
7
+ -- 框架内置角色(ROLES 单一真相:superadmin 安全敏感项 / admin 日常管理 / editor、viewer 页面门)
8
+ INSERT OR IGNORE INTO sys_role (name, module, title, builtin) VALUES ('superadmin', 'framework', '超级管理员', 1);
9
+ INSERT OR IGNORE INTO sys_role (name, module, title, builtin) VALUES ('admin', 'framework', '系统管理员', 1);
10
+ INSERT OR IGNORE INTO sys_role (name, module, title, builtin) VALUES ('editor', 'framework', '编辑', 1);
11
+ INSERT OR IGNORE INTO sys_role (name, module, title, builtin) VALUES ('viewer', 'framework', '访客', 1);
12
+ -- seed 超管绑定(sys_user_role 唯一权威源,D10)
13
+ INSERT OR IGNORE INTO sys_user_role (user_id, role_name) SELECT id, 'superadmin' FROM users WHERE username = 'admin';
14
+ -- 系统设置默认值(PRD §4.3;C4 设置页 / C5 登录方式发现的数据源;market.cdn_url 安全组仅 superadmin 可改)
15
+ INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('framework.site_title', 'framework', 'general', 'string', 'React Antd Admin', 'React Antd Admin', 'system:setting.site_title', 0, 0);
16
+ INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('market.cdn_url', 'market', 'security', 'string', '', '', 'system:setting.market_cdn_url', 0, 0);
17
+ INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('ldap.enabled', 'ldap', 'login', 'bool', '0', '0', 'system:setting.ldap_enabled', 0, 0);
18
+ INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('sso.enabled', 'sso', 'login', 'bool', '0', '0', 'system:setting.sso_enabled', 0, 0);
19
+ INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('ldap_base_dn', 'ldap', 'ldap', 'string', '', '', 'system:setting.ldap_base_dn', 0, 0);
20
+ INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('ldap_uid_attr', 'ldap', 'ldap', 'string', 'uid', 'uid', 'system:setting.ldap_uid_attr', 0, 0);
21
+ INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('ldap_email_attr', 'ldap', 'ldap', 'string', 'mail', 'mail', 'system:setting.ldap_email_attr', 0, 0);
@@ -0,0 +1,86 @@
1
+ import type { Session, TokenBundle } from "./session";
2
+ // LDAP 登录领域服务(D1/AC1-12/PRD §4.5):wiki 九步流程移植 + 本仓 D14/B5 语境。
3
+ // gateway(axis("ldap") 面)与 session(签发出口)均注入——axis 是 oj 运行时全局,Node 侧无此面。
4
+ // 错误两态红线:上游网络/协议错 → 502;凭据拒(0 条/多条/bind false)→ 401;未启用/缺配 → 400。
5
+ // 禁号 → 403(AC1-12:bind 成功也拒签发;LDAP 先查号的枚举面为 D14 已接受项)。
6
+ import { esc, firstAttr } from "./ldap";
7
+
8
+ export interface LdapEntry { dn: string, attrs: Record<string, string[]> }
9
+ export interface LdapGateway {
10
+ search: (base: string, opts: { filter: string, attrs: string[] }) => Promise<LdapEntry[]>
11
+ bind: (dn: string, password: string) => Promise<boolean>
12
+ }
13
+
14
+ /** 运行时网关:oj axis("ldap") 全局(libldap.dylib 插件,PRD §4.5 可行性结论)。 */
15
+ export function runtimeLdapGateway(): LdapGateway {
16
+ const axisLdap = (globalThis as any).axis("ldap");
17
+ return {
18
+ search: (base, opts) => axisLdap.search(base, opts),
19
+ bind: (dn, password) => axisLdap.bind(dn, password),
20
+ };
21
+ }
22
+
23
+ function fail(status: number, message: string): never {
24
+ throw Object.assign(new Error(message), { status });
25
+ }
26
+
27
+ async function setting(key: string): Promise<string> {
28
+ const rows = await db.query("select value from sys_setting where key = ?", [key]) as Array<{ value: string | null }>;
29
+ return rows[0]?.value == null ? "" : String(rows[0].value);
30
+ }
31
+
32
+ export function createLdapLogin(session: Pick<Session, "issueExternal">, gateway: LdapGateway) {
33
+ /** 九步:enabled 闸 → 非空校验 → base_dn → 映射缺省 → search(esc) → bind → JIT → 禁号 → 签发。 */
34
+ return async function ldapLogin(rawUsername: unknown, rawPassword: unknown): Promise<TokenBundle> {
35
+ if ((await setting("ldap.enabled")) !== "1")
36
+ fail(400, "LDAP 未启用");
37
+ const username = String(rawUsername ?? "").trim();
38
+ const password = String(rawPassword ?? "");
39
+ if (!username || !password)
40
+ fail(400, "username/password required");
41
+ const base = await setting("ldap_base_dn");
42
+ if (!base)
43
+ fail(400, "ldap_base_dn 未配置");
44
+ const uidAttr = (await setting("ldap_uid_attr")) || "uid";
45
+ const emailAttr = (await setting("ldap_email_attr")) || "mail";
46
+ let entries: LdapEntry[];
47
+ try {
48
+ entries = await gateway.search(base, { filter: `(${uidAttr}=${esc(username)})`, attrs: [uidAttr, emailAttr] });
49
+ }
50
+ catch {
51
+ fail(502, "ldap upstream failed");
52
+ }
53
+ if (!Array.isArray(entries) || entries.length !== 1)
54
+ fail(401, "invalid credentials"); // 0 条不存在、多条不猜
55
+ let ok = false;
56
+ try {
57
+ ok = await gateway.bind(entries[0].dn, password);
58
+ }
59
+ catch {
60
+ fail(502, "ldap upstream failed");
61
+ }
62
+ if (!ok)
63
+ fail(401, "invalid credentials"); // LDAP 拒凭据是正常返回 false(不抛)
64
+ // JIT find-or-create:键 "ldap:<uid>",占位 hash "!ldap" 恒 verify false(低信任来源不接管本地号);
65
+ // must_change=0——外来号无本地旧口令,悬挂期唯一可用写端(改密)必失败 = 号 brick(设计 §9.4)
66
+ const localName = `ldap:${username}`;
67
+ let rows = await db.query("select id, is_forbidden from users where username = ?", [localName]) as Array<{ id: number, is_forbidden: number }>;
68
+ if (!rows.length) {
69
+ try {
70
+ await db.exec(
71
+ "insert into users (username, password_hash, source, email, must_change, is_forbidden) values (?, '!ldap', 'ldap', ?, 0, 0)",
72
+ [localName, firstAttr(entries[0].attrs, emailAttr) || null],
73
+ );
74
+ }
75
+ catch (e) {
76
+ // 唯一约束兜底(预查与插入间的竞态,同 createUser 模式):撞 username → 回查复用;其余照抛
77
+ if (!/unique|constraint/i.test(e instanceof Error ? e.message : String(e)))
78
+ throw e;
79
+ }
80
+ rows = await db.query("select id, is_forbidden from users where username = ?", [localName]) as Array<{ id: number, is_forbidden: number }>;
81
+ }
82
+ if (Number(rows[0].is_forbidden) === 1)
83
+ fail(403, "account disabled");
84
+ return session.issueExternal(Number(rows[0].id));
85
+ };
86
+ }
@@ -0,0 +1,14 @@
1
+ // LDAP 纯函数(无全局依赖,L2 直测):filter 转义 + 属性首值提取。D1/PRD §4.5 红线移植 wiki。
2
+ // 转义红线:axis("ldap") 的 filter 是拼进目录查询的原始 RFC 4515 字符串,**无参数绑定**——
3
+ // 用户输入拼进 filter 前必须转义(oj-bin docs/api/13-ldap.md「filter 注入」红线)。
4
+
5
+ /** RFC 4515 转义:`*` `(` `)` `\` NUL → `\hh`(两位十六进制)。 */
6
+ export function esc(s: string): string {
7
+ return String(s).replace(/[*(\\)\0]/g, c => `\\${c.charCodeAt(0).toString(16).padStart(2, "0")}`);
8
+ }
9
+
10
+ /** 属性首值(LDAP 多值属性取第一项;缺失/空数组 → 空串)。 */
11
+ export function firstAttr(attrs: Record<string, string[]> | undefined, name: string): string {
12
+ const v = attrs?.[name];
13
+ return Array.isArray(v) && v.length > 0 ? String(v[0]) : "";
14
+ }
@@ -0,0 +1,29 @@
1
+ // 登录限速编排(B5/AC1-26):auth/login 与 auth/ldap-login 共用的 (IP, username) 双维度守卫 + 失败记账。
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";
5
+
6
+ /** 客户端 IP:优先 X-Forwarded-For(反代后),回落 X-Real-IP。 */
7
+ export async function clientIp(): Promise<string> {
8
+ return String((http.headers || {})["x-forwarded-for"] ?? http.headers?.["x-real-ip"] ?? "unknown");
9
+ }
10
+
11
+ /** 登录前守卫:per-IP 粗窗口防撞库 + (IP, username) 连败阻断(429 指数 backoff 在 rate.ts)。 */
12
+ export async function guardLoginThrottle(ip: string, username: string): Promise<void> {
13
+ await rateLimit(`login:ip:${ip}`, 30, 60_000);
14
+ await loginBlocked(`${ip}|${username}`);
15
+ }
16
+
17
+ /** 401 记账:连败 +1(达阈值审计节流事件)+ 失败审计。 */
18
+ export async function noteLoginFailed(ip: string, username: string): Promise<void> {
19
+ const pairKey = `${ip}|${username}`;
20
+ const fails = await loginFailed(pairKey);
21
+ if (fails === 6)
22
+ auditOp({ action: "auth.login.throttle", module: "framework", target: pairKey, level: 4 });
23
+ auditOp({ action: "auth.login.fail", module: "framework", target: username, level: 4 });
24
+ }
25
+
26
+ /** 登录成功清零连败。 */
27
+ export async function noteLoginSucceeded(ip: string, username: string): Promise<void> {
28
+ await loginSucceeded(`${ip}|${username}`);
29
+ }