@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
@@ -3,3 +3,12 @@
3
3
  # 命令报 ERR_PNPM_IGNORED_BUILDS(0.1.0 发布实测)。
4
4
  allowBuilds:
5
5
  esbuild: true
6
+
7
+ # @oj-module/cli 经 `catalog:` 拉 @redocly/cli,但 catalog 协议只作用于 workspace
8
+ # 自有 package.json 的依赖,对 cli(外部包)的 catalog: 传递依赖无效;生成工程
9
+ # 不钉的话会漂到 latest,新发布的版本(如 2.64.1,发布当日)会撞本机
10
+ # minimumReleaseAge supply-chain 策略被拒(ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION)。
11
+ # 用 override 钉死到仓库 pnpm-workspace.yaml 的 minimumReleaseAgeExclude 白名单版本
12
+ # 2.51.0(cli 实测通过的区间 ^2.51.0 内,且发布已久、过策略)。
13
+ overrides:
14
+ '@redocly/cli': 2.51.0
@@ -4,5 +4,10 @@
4
4
  import { z } from "@oj-module/runtime";
5
5
 
6
6
  export const schemas = {
7
-
7
+ "authPasswordPut": {
8
+ body: z.object({
9
+ "old_password": z.string().min(1),
10
+ "new_password": z.string().min(6)
11
+ }),
12
+ },
8
13
  };
@@ -33,7 +33,7 @@ export function createAuthClient(ctx: ModuleContext) {
33
33
  }
34
34
  ctx.register.apiPrefix(API_PREFIX);
35
35
  bindRequest(ctx.utils.request);
36
- return { authLoginPost, authLogoutPost, authRefreshPost };
36
+ return { authLoginPost, authLogoutPost, authRefreshPost, authPasswordPut, authLdapLoginPost };
37
37
  }
38
38
 
39
39
  /**
@@ -108,3 +108,35 @@ export async function authRefreshPost(): Promise<unknown> {
108
108
  throw await toApiError(e);
109
109
  }
110
110
  }
111
+
112
+ export type AuthPasswordPutBody = z.input<(typeof schemas)["authPasswordPut"]["body"]>;
113
+
114
+ export async function authPasswordPut(body: AuthPasswordPutBody): Promise<unknown> {
115
+ const client = ensureReq();
116
+ try {
117
+ const env = await client.put(`auth/password`, { json: body }).json<OjEnvelope<unknown>>();
118
+ // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
119
+ if (typeof env.code === "number" && env.code !== 0)
120
+ throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
121
+ const data = env.data as unknown;
122
+ return data;
123
+ }
124
+ catch (e) {
125
+ throw await toApiError(e);
126
+ }
127
+ }
128
+
129
+ export async function authLdapLoginPost(): Promise<unknown> {
130
+ const client = ensureReq();
131
+ try {
132
+ const env = await client.post(`auth/ldap-login`).json<OjEnvelope<unknown>>();
133
+ // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
134
+ if (typeof env.code === "number" && env.code !== 0)
135
+ throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
136
+ const data = env.data as unknown;
137
+ return data;
138
+ }
139
+ catch (e) {
140
+ throw await toApiError(e);
141
+ }
142
+ }
@@ -34,6 +34,23 @@ export default defineModule({
34
34
  },
35
35
  ],
36
36
  },
37
+ {
38
+ // 首登/重置后强制改密(must_change 悬挂期唯一可用写端);
39
+ // 管理面 403 msg=password change required 时也引导至此
40
+ path: "/change-password",
41
+ handle: {
42
+ layout: "fullscreen",
43
+ hideInMenu: true,
44
+ title: "login:page.changePwd.title",
45
+ },
46
+ children: [
47
+ {
48
+ index: true,
49
+ Component: lazy(() => import("./pages/change-password")),
50
+ handle: { title: "login:page.changePwd.title", hideInMenu: true },
51
+ },
52
+ ],
53
+ },
37
54
  ],
38
55
  i18n: {
39
56
  "zh-CN": () => import("./locales/zh-CN.json"),
@@ -1,10 +1,20 @@
1
1
  {
2
2
  "page": {
3
3
  "title": "Sign in",
4
- "subtitle": "Scaffold seed account: admin / 123456",
4
+ "subtitle": "Please sign in to continue",
5
5
  "username": "Username",
6
6
  "password": "Password",
7
7
  "submit": "Sign in",
8
- "success": "Signed in"
8
+ "success": "Signed in",
9
+ "changePwd": {
10
+ "title": "Change Password",
11
+ "hint": "After first login or an admin reset, you must change your password before using the system.",
12
+ "old": "Current password",
13
+ "new": "New password",
14
+ "confirm": "Confirm new password",
15
+ "min": "At least 6 characters",
16
+ "mismatch": "Passwords do not match",
17
+ "submit": "Change & sign in again"
18
+ }
9
19
  }
10
20
  }
@@ -1,10 +1,20 @@
1
1
  {
2
2
  "page": {
3
3
  "title": "登录",
4
- "subtitle": "脚手架种子账号:admin / 123456",
4
+ "subtitle": "请登录以继续使用系统",
5
5
  "username": "用户名",
6
6
  "password": "密码",
7
7
  "submit": "登 录",
8
- "success": "登录成功"
8
+ "success": "登录成功",
9
+ "changePwd": {
10
+ "title": "修改密码",
11
+ "hint": "首次登录或管理员重置口令后,需先修改密码才能使用系统。",
12
+ "old": "当前密码",
13
+ "new": "新密码",
14
+ "confirm": "确认新密码",
15
+ "min": "至少 6 位",
16
+ "mismatch": "两次输入的新密码不一致",
17
+ "submit": "修改并重新登录"
18
+ }
9
19
  }
10
20
  }
@@ -0,0 +1,74 @@
1
+ import { fetchChangePassword } from "@oj-module/runtime";
2
+
3
+ import { Alert, Button, Form, Input, Typography } from "antd";
4
+ import { useState } from "react";
5
+ import { useTranslation } from "react-i18next";
6
+ import { useNavigate } from "react-router";
7
+
8
+ interface ChangePwdValues {
9
+ old_password: string
10
+ new_password: string
11
+ confirm: string
12
+ }
13
+
14
+ /**
15
+ * 首登/重置后强制改密页(AC1-27):must_change 悬挂期唯一可用写端是
16
+ * PUT /api/auth/password(后端对未改密用户拒绝其余全部端点)。登录响应
17
+ * data.user.must_change / 管理面 403 msg 均会引导到此页。走 runtime 全局
18
+ * request 出口(fetchChangePassword)——auth 无 web 模块、无 client 宿。
19
+ */
20
+ export default function ChangePasswordPage() {
21
+ const { t } = useTranslation();
22
+ const navigate = useNavigate();
23
+ const [error, setError] = useState<string>();
24
+ const [loading, setLoading] = useState(false);
25
+
26
+ async function onFinish(values: ChangePwdValues) {
27
+ setLoading(true);
28
+ setError(undefined);
29
+ try {
30
+ await fetchChangePassword({ old_password: values.old_password, new_password: values.new_password });
31
+ // 改密级联吊销 refresh,旧会话状态陈旧——回登录页重新认证
32
+ navigate("/login");
33
+ }
34
+ catch (e) {
35
+ setError(e instanceof Error ? e.message : String(e));
36
+ }
37
+ finally {
38
+ setLoading(false);
39
+ }
40
+ }
41
+
42
+ return (
43
+ <>
44
+ <Typography.Title level={3}>
45
+ {t("login:page.changePwd.title")}
46
+ </Typography.Title>
47
+ <Typography.Paragraph type="secondary">
48
+ {t("login:page.changePwd.hint")}
49
+ </Typography.Paragraph>
50
+ {error && <Alert style={{ marginBottom: 16 }} type="error" message={error} showIcon />}
51
+ <Form<ChangePwdValues> layout="vertical" onFinish={onFinish}>
52
+ <Form.Item name="old_password" label={t("login:page.changePwd.old")} rules={[{ required: true }]}>
53
+ <Input.Password autoFocus />
54
+ </Form.Item>
55
+ <Form.Item name="new_password" label={t("login:page.changePwd.new")} rules={[{ required: true }, { min: 6 }]} extra={t("login:page.changePwd.min")}>
56
+ <Input.Password />
57
+ </Form.Item>
58
+ <Form.Item
59
+ name="confirm"
60
+ label={t("login:page.changePwd.confirm")}
61
+ dependencies={["new_password"]}
62
+ rules={[{ required: true }, ({ getFieldValue }) => ({
63
+ validator: (_, value) => value === getFieldValue("new_password") ? Promise.resolve() : Promise.reject(new Error(t("login:page.changePwd.mismatch"))),
64
+ })]}
65
+ >
66
+ <Input.Password />
67
+ </Form.Item>
68
+ <Button type="primary" htmlType="submit" block loading={loading}>
69
+ {t("login:page.changePwd.submit")}
70
+ </Button>
71
+ </Form>
72
+ </>
73
+ );
74
+ }
@@ -0,0 +1,90 @@
1
+ import type { AppRouteRecordRaw } from "@oj-module/runtime";
2
+
3
+ import { SettingOutlined } from "@ant-design/icons";
4
+ import { defineModule } from "@oj-module/runtime";
5
+
6
+ import { createElement, lazy } from "react";
7
+
8
+ import { createSystemClient } from "./__client/api";
9
+
10
+ const Users = lazy(() => import("./pages/users"));
11
+ const UserRoles = lazy(() => import("./pages/users/roles"));
12
+ const Roles = lazy(() => import("./pages/roles"));
13
+ const Oplog = lazy(() => import("./pages/oplog"));
14
+ const Settings = lazy(() => import("./pages/settings"));
15
+ const LoginMethods = lazy(() => import("./pages/login-methods"));
16
+
17
+ const routes: AppRouteRecordRaw[] = [
18
+ {
19
+ path: "/system",
20
+ handle: {
21
+ icon: createElement(SettingOutlined),
22
+ title: "system:menu.system",
23
+ order: 100,
24
+ layout: "container",
25
+ },
26
+ children: [
27
+ {
28
+ path: "/system/users",
29
+ Component: Users,
30
+ handle: { title: "system:menu.users" },
31
+ },
32
+ {
33
+ // 成员资格(superadmin 入口从用户管理页进入,菜单不出现)
34
+ path: "/system/users/:uid/roles",
35
+ Component: UserRoles,
36
+ handle: { title: "system:menu.userRoles", hideInMenu: true },
37
+ },
38
+ {
39
+ path: "/system/roles",
40
+ Component: Roles,
41
+ handle: { title: "system:menu.roles" },
42
+ },
43
+ {
44
+ path: "/system/oplog",
45
+ Component: Oplog,
46
+ handle: { title: "system:menu.oplog" },
47
+ },
48
+ {
49
+ path: "/system/settings",
50
+ Component: Settings,
51
+ handle: { title: "system:menu.settings" },
52
+ },
53
+ {
54
+ path: "/system/login-methods",
55
+ Component: LoginMethods,
56
+ handle: { title: "system:menu.loginMethods" },
57
+ },
58
+ ],
59
+ },
60
+ ];
61
+
62
+ /**
63
+ * system 模块:框架管理面(用户/成员资格/角色权限/操作日志/站点设置/登录方式),
64
+ * 数据源为 oj 后端 /api/system/*(模板 api 模块)。__client 不随模板分发——
65
+ * init 后 `oj openapi -o openapi.json` + `ojm api` 生成(生成前本文件 ts 报红属预期,
66
+ * devkit manual §2.2)。
67
+ */
68
+ export default defineModule({
69
+ name: "system",
70
+ description: "系统管理模块(框架管理面)",
71
+ version: "0.1.0",
72
+ peerRuntime: ">=0.0.0",
73
+ // 路由级门禁(B16):非系统管理员不注入 /system 路由、菜单不可见;
74
+ // API 级 403 由 assertSystemAdmin + service 层硬 enforcement 兜底。
75
+ // 角色口径对齐后端 assertSystemAdmin(access.ts:90)——superadmin **或**
76
+ // admin 均可;seed 的 admin 账号只有 superadmin,写 ["admin"] 会把它自己挡在门外。
77
+ config: { requiredRoles: ["admin", "superadmin"] },
78
+ routes,
79
+ i18n: {
80
+ "zh-CN": () => import("./locales/zh-CN.json"),
81
+ "en-US": () => import("./locales/en-US.json"),
82
+ },
83
+ lifecycle: {
84
+ async onInit(ctx) {
85
+ // 构造即接线:登记 /system 前缀 + 绑定 scoped request(D11 前缀收敛),
86
+ // 页面直接 import 生成的端点函数(模块级单槽 req 已由本调用绑定)。
87
+ createSystemClient(ctx);
88
+ },
89
+ },
90
+ });
@@ -0,0 +1,98 @@
1
+ {
2
+ "menu": {
3
+ "system": "System",
4
+ "users": "Users",
5
+ "userRoles": "Membership",
6
+ "roles": "Roles & Permissions",
7
+ "oplog": "Audit Log",
8
+ "settings": "Site Settings",
9
+ "loginMethods": "Login Methods"
10
+ },
11
+ "common": {
12
+ "confirm": "Confirm",
13
+ "cancel": "Cancel",
14
+ "save": "Save",
15
+ "saved": "Saved",
16
+ "search": "Search",
17
+ "reset": "Reset",
18
+ "create": "Create"
19
+ },
20
+ "users": {
21
+ "create": "Create User",
22
+ "resetPwd": "Reset Password",
23
+ "forbidden": "Forbidden",
24
+ "createOk": "User created: password change required on first login",
25
+ "firstLoginHint": "Must change on first login",
26
+ "membership": "Membership",
27
+ "pwdPlaceholder": "At least 6 characters",
28
+ "columns": {
29
+ "username": "Username",
30
+ "displayName": "Display Name",
31
+ "email": "Email",
32
+ "roles": "Roles",
33
+ "status": "Status",
34
+ "actions": "Actions"
35
+ }
36
+ },
37
+ "userRoles": {
38
+ "title": "Membership",
39
+ "saved": "Roles updated",
40
+ "none": "(no roles)"
41
+ },
42
+ "rolesPage": {
43
+ "tabRoles": "Roles",
44
+ "tabPerms": "Permissions",
45
+ "builtin": "Built-in",
46
+ "deprecated": "Deprecated",
47
+ "holders": "Holders",
48
+ "permissions": "Bound Permissions",
49
+ "titleKey": "Title Key",
50
+ "readonlyHint": "Read-only view: roles and bindings are managed by solution tooling (upsertSolutionPerms)"
51
+ },
52
+ "oplog": {
53
+ "filterAction": "Action",
54
+ "filterUsername": "Username (prefix)",
55
+ "filterModule": "Module",
56
+ "filterLimit": "Limit",
57
+ "truncated": "Results truncated by limit; narrow your filters",
58
+ "columns": {
59
+ "time": "Time",
60
+ "username": "Username",
61
+ "action": "Action",
62
+ "module": "Module",
63
+ "target": "Target",
64
+ "detail": "Detail",
65
+ "level": "Level"
66
+ }
67
+ },
68
+ "settingsPage": {
69
+ "secretHint": "Secret items cannot be edited online",
70
+ "saved": "Settings saved"
71
+ },
72
+ "loginMethodsPage": {
73
+ "title": "Enabled login methods",
74
+ "methodLocal": "Local password",
75
+ "methodLdap": "LDAP",
76
+ "methodSso": "SSO"
77
+ },
78
+ "err": {
79
+ "forbidden": "Forbidden",
80
+ "selfAction": "You cannot perform this action on yourself",
81
+ "pwdTooShort": "Password must be at least 6 characters",
82
+ "usernameExists": "Username already exists",
83
+ "usernameReserved": "This username prefix is reserved for external identity sources",
84
+ "mustChange": {
85
+ "title": "Password change required",
86
+ "hint": "This account must change its password before admin features become available.",
87
+ "go": "Change password now"
88
+ }
89
+ },
90
+ "setting": {
91
+ "site_title": "Site Title",
92
+ "ldap_enabled": "Enable LDAP login",
93
+ "sso_enabled": "Enable SSO login",
94
+ "ldap_base_dn": "LDAP Base DN",
95
+ "ldap_uid_attr": "LDAP UID Attribute",
96
+ "ldap_email_attr": "LDAP Email Attribute"
97
+ }
98
+ }
@@ -0,0 +1,98 @@
1
+ {
2
+ "menu": {
3
+ "system": "系统管理",
4
+ "users": "用户管理",
5
+ "userRoles": "成员资格",
6
+ "roles": "角色权限",
7
+ "oplog": "操作日志",
8
+ "settings": "站点设置",
9
+ "loginMethods": "登录方式"
10
+ },
11
+ "common": {
12
+ "confirm": "确认",
13
+ "cancel": "取消",
14
+ "save": "保存",
15
+ "saved": "已保存",
16
+ "search": "查询",
17
+ "reset": "重置",
18
+ "create": "新建"
19
+ },
20
+ "users": {
21
+ "create": "建号",
22
+ "resetPwd": "重置口令",
23
+ "forbidden": "禁用",
24
+ "createOk": "建号成功,首次登录须改密",
25
+ "firstLoginHint": "首登改密",
26
+ "membership": "成员资格",
27
+ "pwdPlaceholder": "至少 6 位",
28
+ "columns": {
29
+ "username": "用户名",
30
+ "displayName": "显示名",
31
+ "email": "邮箱",
32
+ "roles": "角色",
33
+ "status": "状态",
34
+ "actions": "操作"
35
+ }
36
+ },
37
+ "userRoles": {
38
+ "title": "成员资格",
39
+ "saved": "角色已更新",
40
+ "none": "(无角色)"
41
+ },
42
+ "rolesPage": {
43
+ "tabRoles": "角色",
44
+ "tabPerms": "权限点",
45
+ "builtin": "内置",
46
+ "deprecated": "已废弃",
47
+ "holders": "持有者",
48
+ "permissions": "已绑定权限点",
49
+ "titleKey": "标题键",
50
+ "readonlyHint": "只读视图:角色与绑定由解决方案工具链(upsertSolutionPerms)管理"
51
+ },
52
+ "oplog": {
53
+ "filterAction": "操作 (action)",
54
+ "filterUsername": "用户名(前缀匹配)",
55
+ "filterModule": "模块",
56
+ "filterLimit": "条数上限",
57
+ "truncated": "结果已按上限截断,请缩小过滤范围",
58
+ "columns": {
59
+ "time": "时间",
60
+ "username": "用户名",
61
+ "action": "操作",
62
+ "module": "模块",
63
+ "target": "目标",
64
+ "detail": "详情",
65
+ "level": "级别"
66
+ }
67
+ },
68
+ "settingsPage": {
69
+ "secretHint": "敏感项不可在线修改",
70
+ "saved": "设置已保存"
71
+ },
72
+ "loginMethodsPage": {
73
+ "title": "当前启用的登录方式",
74
+ "methodLocal": "本地口令",
75
+ "methodLdap": "LDAP",
76
+ "methodSso": "SSO 单点登录"
77
+ },
78
+ "err": {
79
+ "forbidden": "无权限",
80
+ "selfAction": "不能对自己执行该操作",
81
+ "pwdTooShort": "口令至少 6 位",
82
+ "usernameExists": "用户名已存在",
83
+ "usernameReserved": "该用户名前缀为外部身份源保留",
84
+ "mustChange": {
85
+ "title": "需要先修改密码",
86
+ "hint": "当前账号尚未完成首次改密,管理功能暂不可用。修改完成后将自动返回。",
87
+ "go": "去修改密码"
88
+ }
89
+ },
90
+ "setting": {
91
+ "site_title": "站点标题",
92
+ "ldap_enabled": "启用 LDAP 登录",
93
+ "sso_enabled": "启用 SSO 登录",
94
+ "ldap_base_dn": "LDAP Base DN",
95
+ "ldap_uid_attr": "LDAP UID 属性",
96
+ "ldap_email_attr": "LDAP 邮箱属性"
97
+ }
98
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * 错误归一辅助:兼容两种形态——生成物 client 抛的 ContractApiError(code/msg)
3
+ * 与 ky HTTPError(response.status / data 信封)。判断尽量宽,避免依赖单一
4
+ * 形态的脆弱字段(ky 版本间 HTTPError.data 行为有差异)。
5
+ */
6
+
7
+ /** HTTP 状态:ContractApiError.code 优先,HTTPError 回落 response.status */
8
+ function statusOf(e: unknown): number | undefined {
9
+ const err = e as { code?: number, response?: Response } | null;
10
+ if (typeof err?.code === "number")
11
+ return err.code;
12
+ return err?.response?.status;
13
+ }
14
+
15
+ /** 该错误是否为 403(页面切「无权限/引导」面板的判据) */
16
+ export function isForbidden(e: unknown): boolean {
17
+ return statusOf(e) === 403;
18
+ }
19
+
20
+ /** must_change 判定:悬挂期 403 的 msg 为后端固定文案(access.ts 硬编码) */
21
+ export function isMustChange(e: unknown): boolean {
22
+ const err = e as { msg?: string, data?: { code?: string } } | null;
23
+ return err?.data?.code === "must_change" || (err?.msg ?? "").includes("password change required");
24
+ }
25
+
26
+ /** 人话 msg:优先信封 msg,未知透传后端原文 */
27
+ export function errMsg(e: unknown): string {
28
+ const err = e as { msg?: string, data?: { msg?: string } } | null;
29
+ return err?.msg ?? err?.data?.msg ?? (e instanceof Error ? e.message : String(e));
30
+ }
31
+
32
+ /**
33
+ * 已知业务错误 → i18n 人话;未知透传后端 msg。与后端 fail(status, message)
34
+ * 的 message 逐字匹配(users.ts 等 service 的 fail 文案)。
35
+ */
36
+ export function explainError(e: unknown, t: (key: string) => string): string {
37
+ if (isForbidden(e))
38
+ return t("system:err.forbidden");
39
+ if (isMustChange(e))
40
+ return t("system:err.mustChange.title");
41
+ const msg = errMsg(e);
42
+ if (msg.includes("yourself"))
43
+ return t("system:err.selfAction");
44
+ if (msg.includes("password too short"))
45
+ return t("system:err.pwdTooShort");
46
+ if (msg.includes("username exists"))
47
+ return t("system:err.usernameExists");
48
+ if (msg.includes("reserved"))
49
+ return t("system:err.usernameReserved");
50
+ return msg;
51
+ }
@@ -0,0 +1,35 @@
1
+ import type { TFunction } from "i18next";
2
+
3
+ import { Button, Card, Result } from "antd";
4
+ import { useNavigate } from "react-router";
5
+
6
+ import { isMustChange } from "./errors";
7
+
8
+ /**
9
+ * 页面级「无权限」面板:路由可进但 API 403——规格 §4:渲染引导而非落 error
10
+ * boundary。路由级门禁由 requiredRoles 过滤(B16)负责,本组件不承担。
11
+ *
12
+ * 特判 must_change(后端对未改密用户拒绝全部管理端点):渲染改密引导卡跳
13
+ * /change-password——表单唯一真源在 login 模块,此处不重复实现(DRY)。
14
+ */
15
+ export function ForbiddenPanel({ t, error }: { t: TFunction, error?: unknown }) {
16
+ const navigate = useNavigate();
17
+
18
+ if (isMustChange(error)) {
19
+ return (
20
+ <Card style={{ maxWidth: 480, margin: "48px auto" }}>
21
+ <Result
22
+ status="warning"
23
+ title={t("system:err.mustChange.title")}
24
+ subTitle={t("system:err.mustChange.hint")}
25
+ extra={(
26
+ <Button type="primary" onClick={() => navigate("/change-password")}>
27
+ {t("system:err.mustChange.go")}
28
+ </Button>
29
+ )}
30
+ />
31
+ </Card>
32
+ );
33
+ }
34
+ return <Result status="403" title={t("system:err.forbidden")} />;
35
+ }
@@ -0,0 +1,29 @@
1
+ import { useQuery } from "@tanstack/react-query";
2
+ import { Card, Descriptions, Tag } from "antd";
3
+ import { useTranslation } from "react-i18next";
4
+
5
+ import { systemLoginMethodsGet } from "../../__client/api";
6
+
7
+ const METHOD_KEY: Record<string, string> = {
8
+ local: "system:loginMethodsPage.methodLocal",
9
+ ldap: "system:loginMethodsPage.methodLdap",
10
+ sso: "system:loginMethodsPage.methodSso",
11
+ };
12
+
13
+ /** 登录方式发现(C5/D2,只读):local 恒在,ldap/sso 由设置项开关。 */
14
+ export default function LoginMethodsPage() {
15
+ const { t } = useTranslation();
16
+ const query = useQuery({ queryKey: ["system", "login-methods"], queryFn: systemLoginMethodsGet });
17
+
18
+ return (
19
+ <Card loading={query.isLoading} title={t("system:loginMethodsPage.title")}>
20
+ <Descriptions column={1}>
21
+ <Descriptions.Item label={t("system:menu.loginMethods")}>
22
+ {(query.data?.methods ?? []).map(m => (
23
+ <Tag key={m}>{METHOD_KEY[m] ? t(METHOD_KEY[m]) : m}</Tag>
24
+ ))}
25
+ </Descriptions.Item>
26
+ </Descriptions>
27
+ </Card>
28
+ );
29
+ }