@h-ai/cli 0.1.0-alpha.58 → 0.1.0-alpha.60

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 (93) hide show
  1. package/README.en.md +43 -20
  2. package/README.ja.md +43 -20
  3. package/README.md +43 -20
  4. package/dist/{chunk-WUSFJFW5.js → chunk-P6X63D3I.js} +64 -6
  5. package/dist/chunk-P6X63D3I.js.map +1 -0
  6. package/dist/{chunk-F4IBPVZ5.js → chunk-WWG3U5ZD.js} +20 -2
  7. package/dist/chunk-WWG3U5ZD.js.map +1 -0
  8. package/dist/cli-config-templates-EFABFSCY.js +5 -0
  9. package/dist/{cli-config-templates-6XA7V6CC.js.map → cli-config-templates-EFABFSCY.js.map} +1 -1
  10. package/dist/{cli-skill-templates-45CK4RQ2.js → cli-skill-templates-2KDUACT6.js} +3 -3
  11. package/dist/{cli-skill-templates-45CK4RQ2.js.map → cli-skill-templates-2KDUACT6.js.map} +1 -1
  12. package/dist/index.js +96 -73
  13. package/dist/index.js.map +1 -1
  14. package/package.json +9 -7
  15. package/templates/README.md +8 -0
  16. package/templates/apps/api/apps/{{projectName}}-service/README.md.hbs +12 -0
  17. package/templates/apps/api/apps/{{projectName}}-service/src/app.ts.hbs +4 -7
  18. package/templates/apps/api/apps/{{projectName}}-service/src/lib/server/init.ts.hbs +11 -1
  19. package/templates/apps/api/apps/{{projectName}}-service/tests/service.test.ts.hbs +2 -1
  20. package/templates/apps/api/e2e/api-service.spec.ts.hbs +72 -6
  21. package/templates/apps/api/package.json.hbs +4 -0
  22. package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +10 -0
  23. package/templates/apps/fullstack/apps/{{projectName}}-app/project.inlang/settings.json.hbs +1 -1
  24. package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +10 -0
  25. package/templates/apps/fullstack/apps/{{projectName}}-desktop/project.inlang/settings.json.hbs +1 -1
  26. package/templates/apps/fullstack/apps/{{projectName}}-miniapp/README.md.hbs +2 -0
  27. package/templates/apps/fullstack/apps/{{projectName}}-miniapp/project.inlang/settings.json.hbs +1 -1
  28. package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +10 -0
  29. package/templates/apps/fullstack/apps/{{projectName}}-web/project.inlang/settings.json.hbs +1 -1
  30. package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +63 -1
  31. package/templates/apps/fullstack/packages/{{projectName}}-contract/README.md.hbs +1 -1
  32. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-contract.ts.hbs +8 -1
  33. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-schemas.ts.hbs +1 -1
  34. package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +10 -0
  35. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_iam.yml.hbs +46 -0
  36. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_reach.yml.hbs +21 -0
  37. package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +1 -0
  38. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/admin-service.ts.hbs +1 -1
  39. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/admin-procedures.ts.hbs +2 -2
  40. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/runtime.ts.hbs +7 -3
  41. package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/app-server.test.ts.hbs +11 -4
  42. package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +12 -0
  43. package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/en-US.json.hbs +2 -2
  44. package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/zh-CN.json.hbs +2 -2
  45. package/templates/apps/fullstack/packages/{{projectName}}-shared/package.json.hbs +1 -0
  46. package/templates/apps/fullstack/packages/{{projectName}}-shared/project.inlang/settings.json.hbs +1 -1
  47. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/api-client.ts.hbs +3 -0
  48. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AdminWorkspace.svelte.hbs +32 -23
  49. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/ProfileSettings.svelte.hbs +24 -24
  50. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/UserManagement.svelte.hbs +1 -1
  51. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/management-store.svelte.ts.hbs +20 -20
  52. package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/management-store.test.ts.hbs +7 -8
  53. package/templates/apps/mobile-app/src/App.svelte.hbs +43 -0
  54. package/templates/base/e2e/app.spec.ts.hbs +105 -0
  55. package/templates/base/package.json.hbs +1 -1
  56. package/templates/base/src/hooks.server.ts.hbs +2 -2
  57. package/templates/base/src/lib/server/init.ts.hbs +1 -1
  58. package/templates/base/src/routes/+layout.server.ts.hbs +6 -0
  59. package/templates/base/tests/scaffold.test.ts.hbs +3 -0
  60. package/templates/base/vite.config.ts.hbs +1 -0
  61. package/templates/features/iam/routes-admin/(auth)/+layout.svelte.hbs +2 -16
  62. package/templates/features/iam/routes-admin/(auth)/auth/forgot-password/+page.svelte.hbs +10 -0
  63. package/templates/features/iam/routes-admin/(auth)/auth/login/+page.svelte.hbs +6 -95
  64. package/templates/features/iam/routes-admin/(auth)/auth/register/+page.svelte.hbs +6 -117
  65. package/templates/features/iam/routes-admin/(auth)/auth/reset-password/+page.svelte.hbs +10 -0
  66. package/templates/features/iam/routes-h5/auth/forgot-password/+page.svelte.hbs +10 -0
  67. package/templates/features/iam/routes-h5/auth/login/+page.svelte.hbs +6 -97
  68. package/templates/features/iam/routes-h5/auth/register/+page.svelte.hbs +6 -112
  69. package/templates/features/iam/routes-h5/auth/reset-password/+page.svelte.hbs +10 -0
  70. package/templates/features/iam/routes-shared/api/auth/config/+server.ts +4 -0
  71. package/templates/features/iam/routes-shared/api/auth/login/+server.ts +1 -33
  72. package/templates/features/iam/routes-shared/api/auth/login/ldap/+server.ts +4 -0
  73. package/templates/features/iam/routes-shared/api/auth/login/otp/+server.ts +4 -0
  74. package/templates/features/iam/routes-shared/api/auth/otp/send/+server.ts +4 -0
  75. package/templates/features/iam/routes-shared/api/auth/password-reset/confirm/+server.ts +4 -0
  76. package/templates/features/iam/routes-shared/api/auth/password-reset/request/+server.ts +4 -0
  77. package/templates/features/iam/routes-shared/api/auth/register/+server.ts +1 -23
  78. package/templates/features/iam/routes-website/auth/forgot-password/+page.svelte.hbs +10 -0
  79. package/templates/features/iam/routes-website/auth/login/+page.svelte.hbs +11 -0
  80. package/templates/features/iam/routes-website/auth/register/+page.svelte.hbs +11 -0
  81. package/templates/features/iam/routes-website/auth/reset-password/+page.svelte.hbs +10 -0
  82. package/templates/skills/hai-api-client/SKILL.md +5 -0
  83. package/templates/skills/hai-app-create/SKILL.md +2 -0
  84. package/templates/skills/hai-app-create/development.md +15 -0
  85. package/templates/skills/hai-deploy/SKILL.md +2 -0
  86. package/templates/skills/hai-framework-sync/SKILL.md +4 -0
  87. package/templates/skills/hai-iam/SKILL.md +9 -2
  88. package/templates/skills/hai-iam/reference.md +76 -11
  89. package/templates/skills/hai-reach/SKILL.md +2 -0
  90. package/templates/skills/hai-ui/reference.md +45 -0
  91. package/dist/chunk-F4IBPVZ5.js.map +0 -1
  92. package/dist/chunk-WUSFJFW5.js.map +0 -1
  93. package/dist/cli-config-templates-6XA7V6CC.js +0 -5
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@h-ai/cli",
3
3
  "type": "module",
4
- "version": "0.1.0-alpha.58",
4
+ "version": "0.1.0-alpha.60",
5
5
  "description": "Hai Framework CLI for project scaffolding and code generation.",
6
6
  "license": "Apache-2.0",
7
7
  "keywords": [
@@ -30,9 +30,10 @@
30
30
  "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
31
31
  },
32
32
  "dependencies": {
33
- "@h-ai/core": "0.1.0-alpha.58",
33
+ "@h-ai/core": "0.1.0-alpha.60",
34
34
  "cac": "^7.0.1",
35
35
  "chalk": "^6.0.1",
36
+ "fast-wrap-ansi": "^0.2.2",
36
37
  "fs-extra": "^11.4.1",
37
38
  "handlebars": "^4.7.10",
38
39
  "ora": "^9.4.1",
@@ -40,14 +41,15 @@
40
41
  "yaml": "^2.9.1"
41
42
  },
42
43
  "devDependencies": {
43
- "@h-ai/cache": "0.1.0-alpha.58",
44
- "@h-ai/deploy": "0.1.0-alpha.58",
45
- "@h-ai/reach": "0.1.0-alpha.58",
46
- "@h-ai/reldb": "0.1.0-alpha.58",
47
- "@h-ai/storage": "0.1.0-alpha.58",
44
+ "@h-ai/cache": "0.1.0-alpha.60",
45
+ "@h-ai/deploy": "0.1.0-alpha.60",
46
+ "@h-ai/reach": "0.1.0-alpha.60",
47
+ "@h-ai/reldb": "0.1.0-alpha.60",
48
+ "@h-ai/storage": "0.1.0-alpha.60",
48
49
  "@types/fs-extra": "^11.0.4",
49
50
  "@types/node": "^26.6.5",
50
51
  "@types/prompts": "^2.4.9",
52
+ "fast-string-width": "^3.0.2",
51
53
  "rimraf": "^6.1.3",
52
54
  "tsup": "^8.5.1",
53
55
  "typescript": "^6.0.3",
@@ -142,3 +142,11 @@ import { iam } from '@h-ai/iam'
142
142
  ### 新增 feature 路由
143
143
 
144
144
  在 `features/{featureId}/routes-shared/`(API 路由)或 `features/{featureId}/routes-{appType}/`(UI 页面)下创建文件,然后在 `cli-template-engine.ts` 的 `FEATURE_ROUTE_DIRS` / `FEATURE_APP_ROUTE_DIRS` 中注册该目录。
145
+
146
+ ## 配置驱动认证页面
147
+
148
+ 协议支持配置中的 userAgreementMarkdown/privacyPolicyMarkdown 正文及 URL;同主题优先 Markdown,由安全渲染器在弹窗显示。
149
+
150
+ 选用 IAM 的 admin/h5/website 模板提供 AuthPanel 和标准认证路由;mobile-app 与 fullstack Web/App/Desktop 使用 API 客户端共享动作。修改服务端 config/_iam.yml 即可控制登录方式、注册、重置、协议和品牌,无需改表单。配置 Reach 模板和真实发送 Provider 后启用 OTP/重置。分布式部署使用共享数据库与 Redis。
151
+
152
+ LDAP/Reach 是可选 peer;IAM 不强制启用 Reach。默认 LDAP 客户端由应用选择安装 ldapts。自定义通知回调、协议弹窗、表单 snippet 用法随 hai-iam/hai-ui skills 完整复制到生成项目,不能要求应用读取本仓库私有路径。
@@ -21,3 +21,15 @@ pnpm --filter {{api.servicePackageName}} test
21
21
  - `src/app.ts`:HTTP App 装配
22
22
  - `src/lib/server/init.ts`:模块初始化与关闭
23
23
  - `src/server/procedures/`:应用级 procedures
24
+
25
+ {{#if features.iam}}
26
+ ## IAM 认证配置
27
+
28
+ 服务端 `config/_iam.yml` 统一控制登录、注册、重置、协议与 `ui` 品牌。邮件/短信可用 `_reach.yml` 中的 Provider/标准模板或自定义发送回调。客户端可通过安全公开配置和框架认证动作接入 `AuthPanel`。所有节点共享同一 DB 与 Redis;Memory 仅用于单进程。
29
+
30
+ LDAP/Reach 均可选:基础认证无需安装。默认 LDAP 客户端需在 IAM 服务端 workspace 安装 ldapts,再配置 login.ldap 与连接信息。自定义 onOtpSendEmail/onOtpSendSms/onPasswordResetRequest 回调可独立于 Reach 使用,投递失败须拒绝 Promise。协议在 Modal 显示;LoginForm/RegisterForm 提供 header/extraFields/footer,AuthPanel 透传 loginHeader/loginFields/loginFooter 和 registerHeader/registerFields/registerFooter。配置及例子见生成项目的 hai-iam/hai-ui skills。
31
+
32
+ 协议支持配置中的 userAgreementMarkdown/privacyPolicyMarkdown 正文及 URL;同主题优先 Markdown,由安全渲染器在弹窗显示。
33
+
34
+ 完整契约见生成应用的 `.agents/skills/hai-iam/reference.md`。
35
+ {{/if}}
@@ -59,6 +59,10 @@ export function {{api.serviceAppFactoryName}}(_options: CreateApiServiceAppOptio
59
59
  },
60
60
  {{#if features.iam}}
61
61
  iam,
62
+ refreshCookie,
63
+ {{/if}}
64
+ {{#if features.crypto}}
65
+ transport,
62
66
  {{/if}}
63
67
  middlewares: [
64
68
  {
@@ -71,13 +75,6 @@ export function {{api.serviceAppFactoryName}}(_options: CreateApiServiceAppOptio
71
75
  },
72
76
  ],
73
77
  }
74
- {{#if features.iam}}
75
-
76
- appOptions.refreshCookie = refreshCookie
77
- {{/if}}{{#if features.crypto}}
78
- if (transport)
79
- appOptions.transport = transport
80
- {{/if}}
81
78
 
82
79
  return serv.createApp(appOptions)
83
80
  }
@@ -8,6 +8,7 @@
8
8
  {{/if}}import { core } from '@h-ai/core'
9
9
  {{#if features.crypto}}import { crypto } from '@h-ai/crypto'
10
10
  {{/if}}{{#if features.iam}}import { iam, IamConfigSchema } from '@h-ai/iam'
11
+ {{/if}}{{#if features.reach}}import { reach, ReachConfigSchema } from '@h-ai/reach'
11
12
  {{/if}}{{#if features.db}}import { reldb, ReldbConfigSchema } from '@h-ai/reldb'
12
13
  {{/if}}import { ServConfigSchema } from '@h-ai/serv'
13
14
  {{#if features.storage}}import { storage, StorageConfigSchema } from '@h-ai/storage'
@@ -122,6 +123,14 @@ async function doInit(): Promise<void> {
122
123
  if (!cacheResult.success) {
123
124
  throw new Error(`Cache initialization failed: ${cacheResult.error.message}`)
124
125
  }
126
+ {{/if}}{{#if features.reach}}
127
+
128
+ const reachConfig = core.config.get('reach')
129
+ if (reachConfig) {
130
+ const result = await reach.init(ReachConfigSchema.parse(reachConfig))
131
+ if (!result.success)
132
+ throw new Error('Reach initialization failed')
133
+ }
125
134
  {{/if}}{{#if features.vecdb}}
126
135
 
127
136
  const vecdbResult = await vecdb.init(vecdbConfig)
@@ -130,7 +139,7 @@ async function doInit(): Promise<void> {
130
139
  }
131
140
  {{/if}}{{#if features.iam}}
132
141
 
133
- const iamResult = await iam.init(iamConfig)
142
+ const iamResult = await iam.init({ ...iamConfig{{#if features.reach}}, reach{{/if}} })
134
143
  if (!iamResult.success) {
135
144
  throw new Error(`IAM initialization failed: ${iamResult.error.message}`)
136
145
  }
@@ -159,6 +168,7 @@ export async function closeApp(): Promise<void> {
159
168
  {{#if features.ai}} ['ai', () => ai.close()],
160
169
  {{/if}}{{#if features.storage}} ['storage', () => storage.close()],
161
170
  {{/if}}{{#if features.iam}} ['iam', () => iam.close()],
171
+ {{/if}}{{#if features.reach}} ['reach', () => reach.close()],
162
172
  {{/if}}{{#if features.vecdb}} ['vecdb', () => vecdb.close()],
163
173
  {{/if}}{{#if features.cache}} ['cache', () => cache.close()],
164
174
  {{/if}}{{#if features.db}} ['reldb', () => reldb.close()],
@@ -146,7 +146,8 @@ describe('{{api.servicePackageName}}', () => {
146
146
  {{#if features.iam}}
147
147
  it('registers a user and returns authenticated echo context', async () => {
148
148
  const user = uniqueUser('svc')
149
- const register = await client.iam.auth.register({
149
+ const authActions = apiClient.createAuthActions(client)
150
+ const register = await authActions.register({
150
151
  username: user.username,
151
152
  password: user.password,
152
153
  email: user.email,
@@ -1,8 +1,36 @@
1
- import { apiContract } from '@h-ai/api-contract'
1
+ import process from 'node:process'
2
+ import { apiClient } from '@h-ai/api-client'
3
+ {{#if features.crypto}}
4
+ import { crypto } from '@h-ai/crypto'
5
+ {{/if}}
2
6
  import { expect, test } from '@playwright/test'
3
7
  import { {{api.contractExportName}} } from '{{api.contractPackageName}}'
4
8
 
5
9
  const API_PREFIX = '/api/v1'
10
+ const client = apiClient.create({{api.contractExportName}})
11
+
12
+ test.beforeAll(async () => {
13
+ {{#if features.crypto}}
14
+ expect((await crypto.init()).success).toBe(true)
15
+ {{/if}}
16
+ const result = await client.init({
17
+ baseUrl: `${process.env.BASE_URL || 'http://127.0.0.1:3000'}${API_PREFIX}`,
18
+ {{#if features.iam}}
19
+ auth: { storage: apiClient.tokenStorage.memory() },
20
+ {{/if}}
21
+ {{#if features.crypto}}
22
+ transport: process.env.HAI_SERV_TRANSPORT === 'false' ? undefined : { crypto },
23
+ {{/if}}
24
+ })
25
+ expect(result.success).toBe(true)
26
+ })
27
+
28
+ test.afterAll(async () => {
29
+ await client.close()
30
+ {{#if features.crypto}}
31
+ await crypto.close()
32
+ {{/if}}
33
+ })
6
34
 
7
35
  test('service health endpoint returns ok', async ({ request }) => {
8
36
  const response = await request.get('/health')
@@ -11,14 +39,52 @@ test('service health endpoint returns ok', async ({ request }) => {
11
39
  await expect(response.json()).resolves.toEqual({ status: 'ok' })
12
40
  })
13
41
 
14
- test('service app.info endpoint returns HaiResult', async ({ request }) => {
15
- const response = await request.post(`${API_PREFIX}${apiContract.pathOf({{api.contractExportName}}.app.info)}`)
16
- const body = await response.json()
17
-
18
- expect(response.status()).toBe(200)
42
+ test('service app.info endpoint returns HaiResult', async () => {
43
+ const body = await client.app.info()
19
44
  expect(body.success).toBe(true)
45
+ if (!body.success)
46
+ return
20
47
  expect(body.data.name).toBe(process.env.HAI_CORE_NAME ?? '{{projectName}} API Service')
21
48
  expect(body.data.version).toBe(process.env.HAI_CORE_VERSION ?? '0.1.0')
22
49
  expect(body.data.env).toBe(process.env.HAI_CORE_ENV ?? 'development')
23
50
  expect(typeof body.data.transportEnabled).toBe('boolean')
24
51
  })
52
+ {{#if features.iam}}
53
+
54
+ /** 通过真实 API 验证 IAM 开关、登录、通知和重置,不需要页面适配。 */
55
+ test('configured IAM supports registration, OTP and reset', async ({ request }) => {
56
+ test.setTimeout(60_000)
57
+ const actions = apiClient.createAuthActions(client)
58
+ const publicConfig = await actions.publicConfig()
59
+ expect(publicConfig.success).toBe(true)
60
+ if (!publicConfig.success)
61
+ return
62
+ expect(publicConfig.data?.register.enabled).toBe(true)
63
+ const username = `api${Date.now()}`
64
+ const email = `${username}@example.com`
65
+ const password = 'ApiPassword123'
66
+ expect((await actions.register({ username, email, password })).success).toBe(true)
67
+ expect((await actions.login({ identifier: username, password })).success).toBe(true)
68
+ if (!publicConfig.data?.login.email) {
69
+ // 无 Reach/发送回调时,基础认证仍可用,未就绪的通知入口保持关闭。
70
+ expect((await actions.sendOtp(email)).success).toBe(false)
71
+ return
72
+ }
73
+ if (!process.env.IAM_NOTIFICATION_URL)
74
+ return
75
+ async function received(template: string) {
76
+ const response = await request.get(`${process.env.IAM_NOTIFICATION_URL}/messages?to=${encodeURIComponent(email)}`)
77
+ const messages = await response.json() as { template: string, vars: Record<string, string> }[]
78
+ return messages.findLast(item => item.template === template)?.vars
79
+ }
80
+ expect((await actions.sendOtp(email)).success).toBe(true)
81
+ const otp = await received('otp_email')
82
+ expect(Boolean(otp?.code)).toBe(true)
83
+ expect((await actions.login({ identifier: email, method: 'email', code: otp?.code })).success).toBe(true)
84
+ expect((await actions.requestPasswordReset(email)).success).toBe(true)
85
+ const reset = await received('password_reset')
86
+ expect(Boolean(reset?.token)).toBe(true)
87
+ expect((await actions.confirmPasswordReset(reset?.token ?? '', 'ApiResetPassword456')).success).toBe(true)
88
+ expect((await actions.login({ identifier: username, password: 'ApiResetPassword456' })).success).toBe(true)
89
+ })
90
+ {{/if}}
@@ -25,6 +25,10 @@
25
25
  "{{api.contractPackageName}}": "workspace:*"
26
26
  },
27
27
  "devDependencies": {
28
+ "@h-ai/api-client": "{{haiPackageSpecifier}}",
29
+ {{#if features.crypto}}
30
+ "@h-ai/crypto": "{{haiPackageSpecifier}}",
31
+ {{/if}}
28
32
  "@antfu/eslint-config": "{{api.packageSpecifiers.antfuEslintConfig}}",
29
33
  "@playwright/test": "{{api.packageSpecifiers.playwright}}",
30
34
  "@types/node": "{{api.packageSpecifiers.nodeTypes}}",
@@ -35,4 +35,14 @@ pnpm --filter {{projectName}}-app deploy
35
35
  ```
36
36
 
37
37
  部署前确认 `PUBLIC_API_BASE` 在 `.env` 中指向可访问的后端服务。Android 模拟器访问宿主机通常使用 `http://10.0.2.2:3000`。
38
+
39
+ ## IAM 认证配置
40
+
41
+ 服务端 `config/_iam.yml` 统一控制登录、注册、重置、协议与 `ui` 品牌。邮件/短信可用 `_reach.yml` 中的 Provider/标准模板或自定义发送回调。`AuthPanel` 使用安全公开配置及框架认证动作;无需在页面适配 Token、验证码倒计时和错误提示。所有节点共享同一 DB 与 Redis;Memory 仅用于单进程。
42
+
43
+ LDAP/Reach 均可选:基础认证无需安装。默认 LDAP 客户端需在 IAM 服务端 workspace 安装 ldapts,再配置 login.ldap 与连接信息。自定义 onOtpSendEmail/onOtpSendSms/onPasswordResetRequest 回调可独立于 Reach 使用,投递失败须拒绝 Promise。协议在 Modal 显示;LoginForm/RegisterForm 提供 header/extraFields/footer,AuthPanel 透传 loginHeader/loginFields/loginFooter 和 registerHeader/registerFields/registerFooter。配置及例子见生成项目的 hai-iam/hai-ui skills。
44
+
45
+ 协议支持配置中的 userAgreementMarkdown/privacyPolicyMarkdown 正文及 URL;同主题优先 Markdown,由安全渲染器在弹窗显示。
46
+
47
+ 完整契约见生成应用的 `.agents/skills/hai-iam/reference.md`。
38
48
  {{/if}}
@@ -8,7 +8,7 @@
8
8
  {{/each}}
9
9
  ],
10
10
  "modules": [
11
- "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@latest/dist/index.js"
11
+ "../../node_modules/@inlang/plugin-message-format/dist/index.js"
12
12
  ],
13
13
  "plugin.inlang.messageFormat": {
14
14
  "pathPattern": "./messages/{locale}.json"
@@ -34,4 +34,14 @@ pnpm --filter {{projectName}}-desktop deploy
34
34
  ```
35
35
 
36
36
  打包前需要安装 Rust 工具链和 Tauri 平台依赖。生产环境部署时同步调整 `.env` 中的 `PUBLIC_API_BASE` 与 `src-tauri/tauri.conf.json` 的 `connect-src` 白名单。
37
+
38
+ ## IAM 认证配置
39
+
40
+ 服务端 `config/_iam.yml` 统一控制登录、注册、重置、协议与 `ui` 品牌。邮件/短信可用 `_reach.yml` 中的 Provider/标准模板或自定义发送回调。`AuthPanel` 使用安全公开配置及框架认证动作;无需在页面适配 Token、验证码倒计时和错误提示。所有节点共享同一 DB 与 Redis;Memory 仅用于单进程。
41
+
42
+ LDAP/Reach 均可选:基础认证无需安装。默认 LDAP 客户端需在 IAM 服务端 workspace 安装 ldapts,再配置 login.ldap 与连接信息。自定义 onOtpSendEmail/onOtpSendSms/onPasswordResetRequest 回调可独立于 Reach 使用,投递失败须拒绝 Promise。协议在 Modal 显示;LoginForm/RegisterForm 提供 header/extraFields/footer,AuthPanel 透传 loginHeader/loginFields/loginFooter 和 registerHeader/registerFields/registerFooter。配置及例子见生成项目的 hai-iam/hai-ui skills。
43
+
44
+ 协议支持配置中的 userAgreementMarkdown/privacyPolicyMarkdown 正文及 URL;同主题优先 Markdown,由安全渲染器在弹窗显示。
45
+
46
+ 完整契约见生成应用的 `.agents/skills/hai-iam/reference.md`。
37
47
  {{/if}}
@@ -8,7 +8,7 @@
8
8
  {{/each}}
9
9
  ],
10
10
  "modules": [
11
- "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@latest/dist/index.js"
11
+ "../../node_modules/@inlang/plugin-message-format/dist/index.js"
12
12
  ],
13
13
  "plugin.inlang.messageFormat": {
14
14
  "pathPattern": "./messages/{locale}.json"
@@ -7,6 +7,8 @@
7
7
  - `project.inlang/settings.json`:小程序端 locale 配置
8
8
  - `messages/zh-CN.json`、`messages/en-US.json`:小程序端自有文案
9
9
 
10
+ 消息格式插件复用 workspace 已安装的依赖,从根目录 `node_modules` 加载;生成后先在 workspace 根目录安装依赖,无需从 CDN 加载插件。
11
+
10
12
  后续实现时建议复用 `packages/{{fullstack.contractPackageName}}` 中的 Schema 与路径常量,保持接口真相源一致。
11
13
 
12
14
  协同边界:跨端通用文案/组件放 `packages/{{fullstack.sharedPackageName}}`,小程序独有页面文案放本目录 `messages/`。
@@ -8,7 +8,7 @@
8
8
  {{/each}}
9
9
  ],
10
10
  "modules": [
11
- "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@latest/dist/index.js"
11
+ "../../node_modules/@inlang/plugin-message-format/dist/index.js"
12
12
  ],
13
13
  "plugin.inlang.messageFormat": {
14
14
  "pathPattern": "./messages/{locale}.json"
@@ -27,4 +27,14 @@ pnpm --filter {{projectName}}-web package
27
27
  # 当前模板的 deploy 入口默认执行 package,可替换为 CDN / 容器发布脚本
28
28
  pnpm --filter {{projectName}}-web deploy
29
29
  ```
30
+
31
+ ## IAM 认证配置
32
+
33
+ 服务端 `config/_iam.yml` 统一控制登录、注册、重置、协议与 `ui` 品牌。邮件/短信可用 `_reach.yml` 中的 Provider/标准模板或自定义发送回调。`AuthPanel` 使用安全公开配置及框架认证动作;无需在页面适配 Token、验证码倒计时和错误提示。所有节点共享同一 DB 与 Redis;Memory 仅用于单进程。
34
+
35
+ LDAP/Reach 均可选:基础认证无需安装。默认 LDAP 客户端需在 IAM 服务端 workspace 安装 ldapts,再配置 login.ldap 与连接信息。自定义 onOtpSendEmail/onOtpSendSms/onPasswordResetRequest 回调可独立于 Reach 使用,投递失败须拒绝 Promise。协议在 Modal 显示;LoginForm/RegisterForm 提供 header/extraFields/footer,AuthPanel 透传 loginHeader/loginFields/loginFooter 和 registerHeader/registerFields/registerFooter。配置及例子见生成项目的 hai-iam/hai-ui skills。
36
+
37
+ 协议支持配置中的 userAgreementMarkdown/privacyPolicyMarkdown 正文及 URL;同主题优先 Markdown,由安全渲染器在弹窗显示。
38
+
39
+ 完整契约见生成应用的 `.agents/skills/hai-iam/reference.md`。
30
40
  {{/if}}
@@ -8,7 +8,7 @@
8
8
  {{/each}}
9
9
  ],
10
10
  "modules": [
11
- "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@latest/dist/index.js"
11
+ "../../node_modules/@inlang/plugin-message-format/dist/index.js"
12
12
  ],
13
13
  "plugin.inlang.messageFormat": {
14
14
  "pathPattern": "./messages/{locale}.json"
@@ -1,6 +1,8 @@
1
1
  import type { Page } from '@playwright/test'
2
2
  import { Buffer } from 'node:buffer'
3
3
  import { randomUUID } from 'node:crypto'
4
+ import { mkdir } from 'node:fs/promises'
5
+ import path from 'node:path'
4
6
  import process from 'node:process'
5
7
  import { apiContract } from '@h-ai/api-contract'
6
8
  import { expect, test } from '@playwright/test'
@@ -43,6 +45,66 @@ test('service rejects plaintext business requests', async ({ request }) => {
43
45
  })
44
46
 
45
47
  {{#if fullstack.e2eFrontend}}
48
+ test('shared authentication supports public registration with configuration', async ({ page }) => {
49
+ test.setTimeout(90_000)
50
+ const evidence = process.env.HAI_AUTH_EVIDENCE_DIR || path.resolve('test-results/auth-screenshots')
51
+ await mkdir(evidence, { recursive: true })
52
+ await page.goto('/')
53
+ await expect(page.getByTestId('login-form')).toBeVisible()
54
+ await page.screenshot({ path: path.join(evidence, '{{projectName}}-login.png'), fullPage: true })
55
+ await page.getByRole('button', { name: /立即注册|Register now/i }).click()
56
+ await expect(page.locator('input[name="username"]')).toBeVisible()
57
+ await page.screenshot({ path: path.join(evidence, '{{projectName}}-register.png'), fullPage: true })
58
+ const username = `signup${Date.now()}`
59
+ await page.locator('input[name="username"]').fill(username)
60
+ await page.locator('input[name="email"]').fill(`${username}@example.com`)
61
+ await page.locator('input[type="password"]').nth(0).fill('SignupPassword123')
62
+ await page.locator('input[type="password"]').nth(1).fill('SignupPassword123')
63
+ await page.locator('button[type="submit"]').click()
64
+ await expect(page.getByTestId('settings-page')).toBeVisible()
65
+ expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
66
+ if (!process.env.IAM_NOTIFICATION_URL)
67
+ return
68
+ const email = `${username}@example.com`
69
+ await page.getByTestId('logout').click()
70
+ await page.getByRole('button', { name: /邮件验证码|Email code/i, exact: true }).click()
71
+ await page.screenshot({ path: path.join(evidence, '{{projectName}}-email.png'), fullPage: true })
72
+ await page.locator('#login-username').fill(email)
73
+ await page.getByRole('button', { name: /发送验证码|Send code/i, exact: true }).click()
74
+ await expect(page.getByRole('button', { name: /秒后重发|Resend in/ })).toBeDisabled()
75
+ async function received(template: string) {
76
+ const response = await page.request.get(`${process.env.IAM_NOTIFICATION_URL}/messages?to=${encodeURIComponent(email)}`)
77
+ const messages = await response.json() as { template: string, vars: Record<string, string> }[]
78
+ return messages.findLast(item => item.template === template)?.vars
79
+ }
80
+ const otp = await received('otp_email')
81
+ expect(Boolean(otp?.code)).toBe(true)
82
+ await page.locator('#login-code').fill(otp?.code ?? '')
83
+ await page.locator('button[type="submit"]').click()
84
+ await expect(page.getByTestId('settings-page')).toBeVisible()
85
+ await page.getByTestId('logout').click()
86
+ await page.getByRole('button', { name: /忘记密码|Forgot password/i }).click()
87
+ await page.screenshot({ path: path.join(evidence, '{{projectName}}-forgot.png'), fullPage: true })
88
+ await page.locator('input[type="email"]').fill(email)
89
+ await page.locator('button[type="submit"]').click()
90
+ await expect(page.locator('.alert-success')).toBeVisible()
91
+ const reset = await received('password_reset')
92
+ expect(Boolean(reset?.token)).toBe(true)
93
+ await page.getByRole('button', { name: /已有重置令牌|I have a reset token/i }).click()
94
+ await page.screenshot({ path: path.join(evidence, '{{projectName}}-reset.png'), fullPage: true })
95
+ await page.goto(`/?token=${encodeURIComponent(reset?.token ?? '')}`)
96
+ await expect(page.locator('input[type="password"]').nth(0)).toBeVisible()
97
+ await page.locator('input[type="password"]').nth(0).fill('FullstackReset456')
98
+ await page.locator('input[type="password"]').nth(1).fill('FullstackReset456')
99
+ await page.locator('button[type="submit"]').click()
100
+ await expect(page.locator('.alert-success')).toBeVisible()
101
+ await page.getByRole('button', { name: /^(登录|Sign in|Login)$/i }).click()
102
+ await login(page, username, 'FullstackReset456')
103
+ await expect(page.getByTestId('settings-page')).toBeVisible()
104
+ expect(new URL(page.url()).searchParams.has('token')).toBe(false)
105
+ await page.reload()
106
+ await expect(page.getByTestId('settings-page')).toBeVisible()
107
+ })
46
108
 
47
109
  test('frontend displays configured core metadata and restores the administrator session', async ({ page }) => {
48
110
  await page.goto('/')
@@ -191,7 +253,7 @@ test('administrator and user complete account, avatar, preference, password and
191
253
  await userPage.getByTestId('password-form').getByRole('button', { name: /Change password/i }).click()
192
254
  await expect(userPage.getByTestId('login-form')).toBeVisible()
193
255
  await login(userPage, username, password)
194
- await expect(userPage.getByTestId('login-form')).toContainText(/不正确|incorrect/)
256
+ await expect(userPage.getByTestId('login-form')).toContainText(/不正确|incorrect/i)
195
257
  await login(userPage, username, newPassword)
196
258
  await expect(userPage.getByTestId('settings-page')).toBeVisible()
197
259
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  - `src/app-schemas.ts`:跨接口、跨层复用的输入/数据 Schema 与类型。
8
8
  - `src/app-contract.ts`:app 领域 contract;每个业务 path 直接写在对应接口定义中。
9
- - `src/admin-schemas.ts` / `src/admin-contract.ts`:管理员用户、固定角色、分页审计、个人设置、加密 JSON 头像上传 Schema/类型/路由;认证复用框架的 IAM auth contract,未开放自助注册和其它认证方式。
9
+ - `src/admin-schemas.ts` / `src/admin-contract.ts`:管理员用户、固定角色、分页审计、个人设置、加密 JSON 头像上传 Schema/类型/路由;认证复用框架的 IAM auth contract,通过服务端配置启用注册、密码/LDAP/验证码登录及密码重置。
10
10
  - `src/{{projectName}}-contract.ts`:工程级 contract,供 `@h-ai/serv` 与 `@h-ai/api-client` 共用。
11
11
 
12
12
  只使用一次的输出 Schema 直接内联到 `.output(...)`;同一 contract 文件内重复使用时才保留私有常量。
@@ -56,8 +56,15 @@ export const profileContract = {
56
56
  .output(apiContract.haiResultSchema(AvatarOutputSchema)),
57
57
  }
58
58
 
59
- /** 复用框架认证契约,避免额外开放自助注册和未配置的认证方式。 */
59
+ /** 复用框架认证契约;注册、通知和登录方式由 IAM 配置控制。 */
60
60
  export const authContract = {
61
+ publicConfig: apiContract.iam.auth.publicConfig,
62
+ register: apiContract.iam.auth.register,
63
+ loginWithLdap: apiContract.iam.auth.loginWithLdap,
64
+ loginWithOtp: apiContract.iam.auth.loginWithOtp,
65
+ sendOtp: apiContract.iam.auth.sendOtp,
66
+ requestPasswordReset: apiContract.iam.auth.requestPasswordReset,
67
+ confirmPasswordReset: apiContract.iam.auth.confirmPasswordReset,
61
68
  login: apiContract.iam.auth.login,
62
69
  logout: apiContract.iam.auth.logout,
63
70
  currentUser: apiContract.iam.auth.currentUser,
@@ -28,7 +28,7 @@ export const AdminUsersSchema = z.object({
28
28
  })
29
29
  export const AdminCreateUserSchema = z.object({
30
30
  username: z.string().min(3).max(64).regex(/^[\w-]+$/),
31
- password: z.string().min(8).max(128),
31
+ password: z.string().min(1),
32
32
  email: z.email().optional(),
33
33
  displayName: z.string().max(100).optional(),
34
34
  role: AdminRoleCodeSchema.default('user'),
@@ -33,3 +33,13 @@ CLI 已生成根目录 `.env`,`HAI_ADMIN_DEFAULT_PASSWORD` 为每个项目独
33
33
  - Web 镜像不烘焙 API 地址;启动时使用 `-e PUBLIC_API_BASE=https://api.example.com` 注入,同一镜像可部署到不同环境。未设置时 Web 默认请求自身 origin。
34
34
  - YAML 配置使用 `${ENV_NAME:default}` 接收运行时环境变量;不要把密钥写入镜像。
35
35
  - 将 SQLite/本地存储放入本目录 `data/` 后运行 `pnpm docker:build:data` / `pnpm podman:build:data`,可生成带初始数据的镜像,持续写入仍应使用 volume。
36
+
37
+ ## IAM 认证配置
38
+
39
+ 服务端 `config/_iam.yml` 统一控制登录、注册、重置、协议与 `ui` 品牌。邮件/短信可用 `_reach.yml` 中的 Provider/标准模板或自定义发送回调。`AuthPanel` 使用安全公开配置及框架认证动作;无需在页面适配 Token、验证码倒计时和错误提示。所有节点共享同一 DB 与 Redis;Memory 仅用于单进程。
40
+
41
+ LDAP/Reach 均可选:基础认证无需安装。默认 LDAP 客户端需在 IAM 服务端 workspace 安装 ldapts,再配置 login.ldap 与连接信息。自定义 onOtpSendEmail/onOtpSendSms/onPasswordResetRequest 回调可独立于 Reach 使用,投递失败须拒绝 Promise。协议在 Modal 显示;LoginForm/RegisterForm 提供 header/extraFields/footer,AuthPanel 透传 loginHeader/loginFields/loginFooter 和 registerHeader/registerFields/registerFooter。配置及例子见生成项目的 hai-iam/hai-ui skills。
42
+
43
+ 协议支持配置中的 userAgreementMarkdown/privacyPolicyMarkdown 正文及 URL;同主题优先 Markdown,由安全渲染器在弹窗显示。
44
+
45
+ 完整契约见生成应用的 `.agents/skills/hai-iam/reference.md`。
@@ -19,3 +19,49 @@ rbac:
19
19
  defaultRole: user
20
20
  # 仅由应用创建 admin/user,避免生成无业务用途的默认角色与权限。
21
21
  seedDefaultData: false
22
+
23
+ # 注册与登录安全
24
+ register:
25
+ enabled: true
26
+ defaultEnabled: true
27
+ security:
28
+ maxLoginAttempts: 5
29
+ lockoutDuration: 900
30
+
31
+ # LDAP 可选:安装 ldapts 后开启 login.ldap,并声明 ldap 连接配置或注入工厂。
32
+ # OTP 需启用 login.otp,并选择 Reach 标准模板或自定义发送回调。
33
+ otp:
34
+ email: true
35
+ sms: true
36
+ length: 6
37
+ expiresIn: 300
38
+ maxAttempts: 3
39
+ resendInterval: 60
40
+ passwordReset:
41
+ enabled: true
42
+ mode: email # phone 使用 password_reset_sms 模板
43
+ tokenExpiresIn: 3600
44
+ maxAttempts: 3
45
+ # resetUrl: https://example.com/auth/reset-password
46
+ agreements:
47
+ showOnRegister: true
48
+ showOnLogin: false
49
+ # Markdown 正文由公开配置传给所有客户端,在弹窗内安全渲染。
50
+ userAgreementMarkdown: |
51
+ # 用户协议示例
52
+
53
+ 请妥善保管账户凭据,并按应用提供的功能使用服务。
54
+ privacyPolicyMarkdown: |
55
+ # 隐私政策示例
56
+
57
+ 注册资料用于账户管理,会话信息用于身份认证。
58
+ # userAgreementUrl: https://example.com/terms
59
+ # privacyPolicyUrl: https://example.com/privacy
60
+ ui:
61
+ variant: card
62
+ # title: Account center
63
+ # subtitle: Welcome back
64
+ # brandTitle: Your application
65
+ # brandText: Sign in securely
66
+ # logoUrl: /logo.svg
67
+ # illustrationUrl: /auth-illustration.svg
@@ -0,0 +1,21 @@
1
+ # 开发触达示例;生产环境改为 SMTP、短信或 API Provider。
2
+ providers:
3
+ - name: email
4
+ type: console
5
+ - name: sms
6
+ type: console
7
+ templates:
8
+ - name: otp_email
9
+ provider: email
10
+ subject: Verification code
11
+ body: 'Your verification code is {code}.'
12
+ - name: otp_sms
13
+ provider: sms
14
+ body: 'Your verification code is {code}.'
15
+ - name: password_reset
16
+ provider: email
17
+ subject: Password reset
18
+ body: 'Reset your password: {resetUrl}. Expires at {expiresAt}.'
19
+ - name: password_reset_sms
20
+ provider: sms
21
+ body: 'Reset your password: {resetUrl}. Expires at {expiresAt}.'
@@ -34,6 +34,7 @@
34
34
  "@h-ai/core": "{{haiPackageSpecifier}}",
35
35
  "@h-ai/crypto": "{{haiPackageSpecifier}}",
36
36
  "@h-ai/iam": "{{haiPackageSpecifier}}",
37
+ "@h-ai/reach": "{{haiPackageSpecifier}}",
37
38
  "@h-ai/reldb": "{{haiPackageSpecifier}}",
38
39
  "@h-ai/serv": "{{haiPackageSpecifier}}",
39
40
  "@h-ai/storage": "{{haiPackageSpecifier}}",
@@ -113,7 +113,7 @@ export async function createAdminUser(input: AdminCreateUser, context: ServConte
113
113
  return role
114
114
  if (!role.data)
115
115
  return err(HaiCommonError.NOT_FOUND, serverM('notFound', { locale: context.locale }))
116
- const registered = await iam.user.register(input)
116
+ const registered = await iam.user.createUser(input)
117
117
  if (!registered.success)
118
118
  return registered
119
119
  const userId = registered.data.user.id
@@ -20,7 +20,7 @@ import { serverM } from '../messages.js'
20
20
 
21
21
  /** 身份认证沿用框架公共契约与 refresh-cookie 传输。 */
22
22
  export function createAuthProcedures() {
23
- const { currentUser, refresh } = createIamProcedures({ iam }).auth
23
+ const { currentUser, refresh, publicConfig, loginWithOtp, loginWithLdap, register, sendOtp, requestPasswordReset, confirmPasswordReset } = createIamProcedures({ iam }).auth
24
24
  const { login, logout, changePassword } = authContract
25
25
  // 仅覆写需要业务审计的认证操作,其余直接复用框架;未选认证入口不挂载。
26
26
  const audited = serv.implement({ login, logout, changePassword }).context<ServContext>().route('login', async ({ input, context }) => {
@@ -42,7 +42,7 @@ export function createAuthProcedures() {
42
42
  await recordAudit(context, 'password-change', 'profile', context.session.userId)
43
43
  return result
44
44
  }).build()
45
- return { ...audited, currentUser, refresh }
45
+ return { ...audited, currentUser, refresh, publicConfig, loginWithOtp, loginWithLdap, register, sendOtp, requestPasswordReset, confirmPasswordReset }
46
46
  }
47
47
 
48
48
  export function createAdminProcedures() {