@h-ai/cli 0.1.0-alpha.59 → 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.
- package/README.en.md +9 -1
- package/README.ja.md +9 -1
- package/README.md +9 -1
- package/dist/{chunk-WUSFJFW5.js → chunk-P6X63D3I.js} +64 -6
- package/dist/chunk-P6X63D3I.js.map +1 -0
- package/dist/cli-config-templates-EFABFSCY.js +5 -0
- package/dist/{cli-config-templates-6XA7V6CC.js.map → cli-config-templates-EFABFSCY.js.map} +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/templates/README.md +8 -0
- package/templates/apps/api/apps/{{projectName}}-service/README.md.hbs +12 -0
- package/templates/apps/api/apps/{{projectName}}-service/src/app.ts.hbs +4 -7
- package/templates/apps/api/apps/{{projectName}}-service/src/lib/server/init.ts.hbs +11 -1
- package/templates/apps/api/apps/{{projectName}}-service/tests/service.test.ts.hbs +2 -1
- package/templates/apps/api/e2e/api-service.spec.ts.hbs +72 -6
- package/templates/apps/api/package.json.hbs +4 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +10 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/project.inlang/settings.json.hbs +1 -1
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +10 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/project.inlang/settings.json.hbs +1 -1
- package/templates/apps/fullstack/apps/{{projectName}}-miniapp/README.md.hbs +2 -0
- package/templates/apps/fullstack/apps/{{projectName}}-miniapp/project.inlang/settings.json.hbs +1 -1
- package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +10 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/project.inlang/settings.json.hbs +1 -1
- package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +63 -1
- package/templates/apps/fullstack/packages/{{projectName}}-contract/README.md.hbs +1 -1
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-contract.ts.hbs +8 -1
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-schemas.ts.hbs +1 -1
- package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +10 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_iam.yml.hbs +46 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_reach.yml.hbs +21 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +1 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/admin-service.ts.hbs +1 -1
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/admin-procedures.ts.hbs +2 -2
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/runtime.ts.hbs +7 -3
- package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/app-server.test.ts.hbs +11 -4
- package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +12 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/en-US.json.hbs +2 -2
- package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/zh-CN.json.hbs +2 -2
- package/templates/apps/fullstack/packages/{{projectName}}-shared/package.json.hbs +1 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/project.inlang/settings.json.hbs +1 -1
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/api-client.ts.hbs +3 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AdminWorkspace.svelte.hbs +32 -23
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/ProfileSettings.svelte.hbs +24 -24
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/UserManagement.svelte.hbs +1 -1
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/management-store.svelte.ts.hbs +20 -20
- package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/management-store.test.ts.hbs +7 -8
- package/templates/apps/mobile-app/src/App.svelte.hbs +43 -0
- package/templates/base/e2e/app.spec.ts.hbs +105 -0
- package/templates/base/package.json.hbs +1 -1
- package/templates/base/src/hooks.server.ts.hbs +2 -2
- package/templates/base/src/lib/server/init.ts.hbs +1 -1
- package/templates/base/src/routes/+layout.server.ts.hbs +6 -0
- package/templates/base/tests/scaffold.test.ts.hbs +3 -0
- package/templates/base/vite.config.ts.hbs +1 -0
- package/templates/features/iam/routes-admin/(auth)/+layout.svelte.hbs +2 -16
- package/templates/features/iam/routes-admin/(auth)/auth/forgot-password/+page.svelte.hbs +10 -0
- package/templates/features/iam/routes-admin/(auth)/auth/login/+page.svelte.hbs +6 -95
- package/templates/features/iam/routes-admin/(auth)/auth/register/+page.svelte.hbs +6 -117
- package/templates/features/iam/routes-admin/(auth)/auth/reset-password/+page.svelte.hbs +10 -0
- package/templates/features/iam/routes-h5/auth/forgot-password/+page.svelte.hbs +10 -0
- package/templates/features/iam/routes-h5/auth/login/+page.svelte.hbs +6 -97
- package/templates/features/iam/routes-h5/auth/register/+page.svelte.hbs +6 -112
- package/templates/features/iam/routes-h5/auth/reset-password/+page.svelte.hbs +10 -0
- package/templates/features/iam/routes-shared/api/auth/config/+server.ts +4 -0
- package/templates/features/iam/routes-shared/api/auth/login/+server.ts +1 -33
- package/templates/features/iam/routes-shared/api/auth/login/ldap/+server.ts +4 -0
- package/templates/features/iam/routes-shared/api/auth/login/otp/+server.ts +4 -0
- package/templates/features/iam/routes-shared/api/auth/otp/send/+server.ts +4 -0
- package/templates/features/iam/routes-shared/api/auth/password-reset/confirm/+server.ts +4 -0
- package/templates/features/iam/routes-shared/api/auth/password-reset/request/+server.ts +4 -0
- package/templates/features/iam/routes-shared/api/auth/register/+server.ts +1 -23
- package/templates/features/iam/routes-website/auth/forgot-password/+page.svelte.hbs +10 -0
- package/templates/features/iam/routes-website/auth/login/+page.svelte.hbs +11 -0
- package/templates/features/iam/routes-website/auth/register/+page.svelte.hbs +11 -0
- package/templates/features/iam/routes-website/auth/reset-password/+page.svelte.hbs +10 -0
- package/templates/skills/hai-api-client/SKILL.md +5 -0
- package/templates/skills/hai-iam/SKILL.md +9 -2
- package/templates/skills/hai-iam/reference.md +76 -11
- package/templates/skills/hai-reach/SKILL.md +2 -0
- package/templates/skills/hai-ui/reference.md +45 -0
- package/dist/chunk-WUSFJFW5.js.map +0 -1
- 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.
|
|
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,7 +30,7 @@
|
|
|
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.
|
|
33
|
+
"@h-ai/core": "0.1.0-alpha.60",
|
|
34
34
|
"cac": "^7.0.1",
|
|
35
35
|
"chalk": "^6.0.1",
|
|
36
36
|
"fast-wrap-ansi": "^0.2.2",
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
"yaml": "^2.9.1"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@h-ai/cache": "0.1.0-alpha.
|
|
45
|
-
"@h-ai/deploy": "0.1.0-alpha.
|
|
46
|
-
"@h-ai/reach": "0.1.0-alpha.
|
|
47
|
-
"@h-ai/reldb": "0.1.0-alpha.
|
|
48
|
-
"@h-ai/storage": "0.1.0-alpha.
|
|
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",
|
|
49
49
|
"@types/fs-extra": "^11.0.4",
|
|
50
50
|
"@types/node": "^26.6.5",
|
|
51
51
|
"@types/prompts": "^2.4.9",
|
package/templates/README.md
CHANGED
|
@@ -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
|
|
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
|
|
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 (
|
|
15
|
-
const
|
|
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
|
-
"
|
|
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}}
|
package/templates/apps/fullstack/apps/{{projectName}}-desktop/project.inlang/settings.json.hbs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{{/each}}
|
|
9
9
|
],
|
|
10
10
|
"modules": [
|
|
11
|
-
"
|
|
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/`。
|
package/templates/apps/fullstack/apps/{{projectName}}-miniapp/project.inlang/settings.json.hbs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{{/each}}
|
|
9
9
|
],
|
|
10
10
|
"modules": [
|
|
11
|
-
"
|
|
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
|
-
"
|
|
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 文件内重复使用时才保留私有常量。
|
package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-contract.ts.hbs
CHANGED
|
@@ -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(
|
|
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}}",
|
package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/admin-service.ts.hbs
CHANGED
|
@@ -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.
|
|
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() {
|
|
@@ -7,6 +7,7 @@ import { cache, CacheConfigSchema } from '@h-ai/cache'
|
|
|
7
7
|
import { core } from '@h-ai/core'
|
|
8
8
|
import { crypto } from '@h-ai/crypto'
|
|
9
9
|
import { iam, IamConfigSchema } from '@h-ai/iam'
|
|
10
|
+
import { reach, ReachConfigSchema } from '@h-ai/reach'
|
|
10
11
|
import { reldb, ReldbConfigSchema } from '@h-ai/reldb'
|
|
11
12
|
import { ServConfigSchema } from '@h-ai/serv'
|
|
12
13
|
import { storage, StorageConfigSchema } from '@h-ai/storage'
|
|
@@ -39,7 +40,10 @@ async function runInitialization(): Promise<void> {
|
|
|
39
40
|
requireInitialized(await cache.init(cacheConfig), 'cache')
|
|
40
41
|
requireInitialized(await crypto.init(), 'crypto')
|
|
41
42
|
requireInitialized(await storage.init(storageConfig), 'storage')
|
|
42
|
-
|
|
43
|
+
const reachConfig = core.config.get('reach')
|
|
44
|
+
if (reachConfig)
|
|
45
|
+
requireInitialized(await reach.init(ReachConfigSchema.parse(reachConfig)), 'reach')
|
|
46
|
+
requireInitialized(await iam.init({ ...iamConfig, reach }), 'iam')
|
|
43
47
|
requireInitialized(await audit.init(auditConfig), 'audit')
|
|
44
48
|
await ensureRolesAndAdmin()
|
|
45
49
|
initialized = true
|
|
@@ -71,7 +75,7 @@ async function ensureRolesAndAdmin(): Promise<void> {
|
|
|
71
75
|
const password = process.env.HAI_ADMIN_DEFAULT_PASSWORD?.trim()
|
|
72
76
|
if (!password)
|
|
73
77
|
throw new Error('Set HAI_ADMIN_DEFAULT_PASSWORD before the first start to create the administrator account')
|
|
74
|
-
const registered = requireInitialized(await iam.user.
|
|
78
|
+
const registered = requireInitialized(await iam.user.createUser({ username: 'admin', password, displayName: serverM('admin') }), 'iam-bootstrap')
|
|
75
79
|
const adminRole = requireInitialized(await iam.authz.getRoleByCode('admin'), 'iam-role')
|
|
76
80
|
if (!adminRole)
|
|
77
81
|
throw new Error('Administrator role is missing')
|
|
@@ -87,7 +91,7 @@ async function ensureRolesAndAdmin(): Promise<void> {
|
|
|
87
91
|
|
|
88
92
|
/** 关闭顺序与依赖初始化顺序相反,同时覆盖启动失败路径。 */
|
|
89
93
|
export async function closeModules(): Promise<void> {
|
|
90
|
-
for (const [name, module] of [['audit', audit], ['iam', iam], ['storage', storage], ['crypto', crypto], ['cache', cache], ['reldb', reldb]] as const) {
|
|
94
|
+
for (const [name, module] of [['audit', audit], ['iam', iam], ['reach', reach], ['storage', storage], ['crypto', crypto], ['cache', cache], ['reldb', reldb]] as const) {
|
|
91
95
|
if (!module.isInitialized)
|
|
92
96
|
continue
|
|
93
97
|
const result = await module.close()
|