@h-ai/cli 0.1.0-alpha.64 → 0.1.0-alpha.66
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 +31 -9
- package/README.ja.md +30 -8
- package/README.md +38 -16
- package/dist/{chunk-X3RSYNBG.js → chunk-FWN5B54L.js} +94 -4
- package/dist/chunk-FWN5B54L.js.map +1 -0
- package/dist/chunk-QYX6V7H2.js +5613 -0
- package/dist/chunk-QYX6V7H2.js.map +1 -0
- package/dist/cli-config-templates-3XSFIXBP.js +5 -0
- package/dist/{cli-config-templates-QRF6B5T4.js.map → cli-config-templates-3XSFIXBP.js.map} +1 -1
- package/dist/{cli-skill-templates-EZFW7D43.js → cli-skill-templates-TSYEQTCY.js} +3 -3
- package/dist/{cli-skill-templates-EZFW7D43.js.map → cli-skill-templates-TSYEQTCY.js.map} +1 -1
- package/dist/index.js +733 -210
- package/dist/index.js.map +1 -1
- package/dist/{src-DUGLX4YB.js → src-RDQ2EIBT.js} +1054 -660
- package/dist/{src-DUGLX4YB.js.map → src-RDQ2EIBT.js.map} +1 -1
- package/package.json +9 -7
- package/templates/apps/admin/src/routes/+layout.svelte.hbs +4 -2
- package/templates/apps/api/.dockerignore.hbs +4 -0
- package/templates/apps/api/.gitignore.hbs +6 -5
- package/templates/apps/api/Dockerfile.hbs +5 -8
- package/templates/apps/api/apps/{{projectName}}-contract/package.json.hbs +2 -0
- package/templates/apps/api/apps/{{projectName}}-service/README.md.hbs +2 -2
- package/templates/apps/api/apps/{{projectName}}-service/config/_core.yml.hbs +2 -2
- package/templates/apps/api/apps/{{projectName}}-service/config/_serv.yml.hbs +4 -4
- package/templates/apps/api/apps/{{projectName}}-service/package.json.hbs +8 -2
- package/templates/apps/api/hai.build.json.hbs +1 -0
- package/templates/apps/api/package.json.hbs +13 -7
- package/templates/apps/fullstack/.dockerignore.hbs +4 -3
- package/templates/apps/fullstack/.gitignore.hbs +24 -10
- package/templates/apps/fullstack/Dockerfile.hbs +6 -8
- package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +0 -3
- package/templates/apps/fullstack/apps/{{projectName}}-app/package.json.hbs +5 -1
- package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +8 -4
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +3 -6
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/package.json.hbs +6 -2
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/src/lib.rs.hbs +73 -2
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/tauri.conf.json.hbs +2 -2
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +8 -4
- package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +2 -6
- package/templates/apps/fullstack/apps/{{projectName}}-web/package.json.hbs +5 -1
- package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +8 -4
- package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +24 -1
- package/templates/apps/fullstack/hai.build.json.hbs +7 -0
- package/templates/apps/fullstack/package.json.hbs +16 -11
- package/templates/apps/fullstack/packages/{{projectName}}-contract/README.md.hbs +2 -2
- package/templates/apps/fullstack/packages/{{projectName}}-contract/package.json.hbs +2 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-contract.ts.hbs +1 -1
- package/templates/apps/fullstack/packages/{{projectName}}-contract/tests/contract.test.ts.hbs +8 -14
- package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +8 -6
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_reach.yml.hbs +8 -8
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +3 -3
- package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +8 -2
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/app-server.ts.hbs +4 -4
- package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +4 -2
- package/templates/apps/fullstack/packages/{{projectName}}-shared/package.json.hbs +2 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AdminWorkspace.svelte.hbs +14 -12
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +1 -1
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/management-store.svelte.ts.hbs +17 -3
- package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/management-store.test.ts.hbs +15 -5
- package/templates/apps/fullstack/playwright.config.ts.hbs +6 -1
- package/templates/apps/fullstack/pnpm-workspace.yaml.hbs +0 -1
- package/templates/apps/h5/src/routes/+layout.svelte.hbs +13 -13
- package/templates/apps/mobile-app/index.html.hbs +1 -0
- package/templates/apps/mobile-app/public/runtime-config.js +1 -0
- package/templates/apps/mobile-app/src/App.svelte.hbs +46 -117
- package/templates/apps/mobile-app/src/lib/api.ts.hbs +2 -1
- package/templates/apps/mobile-app/src/lib/app-config.ts.hbs +3 -2
- package/templates/apps/mobile-app/src/lib/pages/home.svelte.hbs +58 -0
- package/templates/apps/mobile-app/src/lib/pages/profile.svelte.hbs +13 -0
- package/templates/apps/mobile-app/src/lib/pages/tasks.svelte.hbs +16 -0
- package/templates/base/.dockerignore.hbs +4 -0
- package/templates/base/.gitignore.hbs +20 -5
- package/templates/base/Dockerfile.hbs +10 -11
- package/templates/base/docker/nginx.conf.hbs +6 -0
- package/templates/base/e2e/app.spec.ts.hbs +14 -0
- package/templates/base/hai.build.json.hbs +1 -0
- package/templates/base/package.json.hbs +25 -17
- package/templates/base/svelte.config.js.hbs +2 -0
- package/templates/base/tests/scaffold.test.ts.hbs +9 -3
- package/templates/base/vite.config.ts.hbs +11 -5
- package/templates/shared/.gitattributes +5 -0
- package/templates/shared/container-runtime-config.sh +30 -0
- package/templates/shared/hash-router.svelte.ts +83 -0
- package/templates/shared/run-e2e.mjs +65 -0
- package/templates/shared/vite-runtime-config.ts +54 -0
- package/templates/skills/AGENTS.md +15 -1
- package/templates/skills/bridges/admin/AGENTS.md +17 -1
- package/templates/skills/bridges/api/AGENTS.md +17 -3
- package/templates/skills/bridges/fullstack/AGENTS.md.hbs +35 -11
- package/templates/skills/bridges/h5/AGENTS.md +17 -1
- package/templates/skills/bridges/mobile-app/AGENTS.md +19 -3
- package/templates/skills/bridges/website/AGENTS.md +17 -1
- package/templates/skills/hai-ai/reference.md +2 -2
- package/templates/skills/hai-app-create/SKILL.md +5 -3
- package/templates/skills/hai-app-create/development.md +128 -2
- package/templates/skills/hai-app-create/reference.md +4 -4
- package/templates/skills/hai-app-review/SKILL.md +2 -2
- package/templates/skills/hai-app-tests/SKILL.md +2 -1
- package/templates/skills/hai-app-tests/reference.md +1 -1
- package/templates/skills/hai-build/SKILL.md +16 -1
- package/templates/skills/hai-core/SKILL.md +2 -1
- package/templates/skills/hai-deploy/SKILL.md +52 -252
- package/templates/skills/hai-deploy/middleware.md +88 -0
- package/templates/skills/hai-framework-sync/SKILL.md +1 -1
- package/templates/skills/hai-framework-sync/sync.md +1 -1
- package/templates/skills/hai-reach/reference.md +8 -8
- package/templates/skills/hai-spec-implement/SKILL.md +12 -9
- package/templates/skills/hai-spec-implement/changes.md +67 -0
- package/templates/skills/hai-spec-implement/templates.md +25 -3
- package/templates/skills/hai-spec-implement/workflow.md +7 -6
- package/templates/skills/hai-spec-review/SKILL.md +14 -5
- package/templates/skills/hai-spec-review/checklist.md +4 -3
- package/templates/skills/hai-storage/SKILL.md +2 -0
- package/templates/skills/hai-ui/reference.md +2 -0
- package/dist/chunk-WW2MIO6T.js +0 -992
- package/dist/chunk-WW2MIO6T.js.map +0 -1
- package/dist/chunk-X3RSYNBG.js.map +0 -1
- package/dist/cli-config-templates-QRF6B5T4.js +0 -5
- /package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/{components/AuditManagement.svelte.hbs → pages/audit.svelte.hbs} +0 -0
- /package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/{components/RoleManagement.svelte.hbs → pages/roles.svelte.hbs} +0 -0
- /package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/{components/ProfileSettings.svelte.hbs → pages/settings.svelte.hbs} +0 -0
- /package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/{components/UserManagement.svelte.hbs → pages/users.svelte.hbs} +0 -0
|
@@ -9,13 +9,16 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"paraglide:compile": "paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide",
|
|
11
11
|
"paraglide:watch": "paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide --watch",
|
|
12
|
+
"dev:test": "vite --mode test",
|
|
13
|
+
"dev:prod": "vite --mode prod",
|
|
12
14
|
"dev": "vite dev --host 127.0.0.1 --port {{fullstack.ports.web}}",
|
|
13
15
|
"build": "pnpm paraglide:compile && vite build",
|
|
14
16
|
"package": "pnpm build",
|
|
15
|
-
"deploy": "pnpm package",
|
|
16
17
|
"preview": "vite preview --host 127.0.0.1 --port {{fullstack.ports.web}} --strictPort",
|
|
18
|
+
"check": "pnpm typecheck",
|
|
17
19
|
"typecheck": "pnpm paraglide:compile && svelte-check --tsconfig ./tsconfig.json",
|
|
18
20
|
"lint": "eslint .",
|
|
21
|
+
"lint:fix": "eslint . --fix",
|
|
19
22
|
"test": "vitest run --passWithNoTests",
|
|
20
23
|
"test:watch": "vitest",
|
|
21
24
|
"clean": "rimraf dist build .turbo src/lib/paraglide"
|
|
@@ -26,6 +29,7 @@
|
|
|
26
29
|
},
|
|
27
30
|
"devDependencies": {
|
|
28
31
|
"@antfu/eslint-config": "{{fullstack.packageSpecifiers.antfuEslintConfig}}",
|
|
32
|
+
"@h-ai/core": "{{haiPackageSpecifier}}",
|
|
29
33
|
"@iconify-json/tabler": "{{fullstack.packageSpecifiers.iconifyJsonTabler}}",
|
|
30
34
|
"@iconify/tailwind4": "{{fullstack.packageSpecifiers.iconifyTailwind}}",
|
|
31
35
|
"@inlang/paraglide-js": "{{fullstack.packageSpecifiers.inlangParaglide}}",
|
|
@@ -2,21 +2,25 @@
|
|
|
2
2
|
{{#if fullstack.frontends.web}}
|
|
3
3
|
import process from 'node:process'
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { core } from '@h-ai/core'
|
|
5
6
|
import { haiOptimizeExclude, haiPrebundledDeps } from '@h-ai/ui/vite'
|
|
6
7
|
import { paraglideVitePlugin } from '@inlang/paraglide-js'
|
|
7
8
|
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
|
8
9
|
import tailwindcss from '@tailwindcss/vite'
|
|
9
|
-
import { loadEnv } from 'vite'
|
|
10
10
|
import { defineConfig } from 'vitest/config'
|
|
11
11
|
|
|
12
12
|
const envDir = fileURLToPath(new URL('../../', import.meta.url))
|
|
13
13
|
|
|
14
|
-
export default defineConfig(({ mode }) => {
|
|
15
|
-
const
|
|
14
|
+
export default defineConfig(({ mode, command }) => {
|
|
15
|
+
const envResult = command === 'build' ? { success: true as const, data: {} as Record<string, string> } : core.config.loadEnvFiles({ mode, envDir })
|
|
16
|
+
if (!envResult.success)
|
|
17
|
+
throw new Error(envResult.error.message)
|
|
18
|
+
const env = envResult.data
|
|
19
|
+
Object.assign(process.env, env)
|
|
16
20
|
const target = (env.PUBLIC_API_BASE || process.env.SERVICE_BASE_URL || 'http://127.0.0.1:3000').replace(/\/api\/v1\/?$/, '')
|
|
17
21
|
const proxy = { '/api': { target, changeOrigin: true } }
|
|
18
22
|
return {
|
|
19
|
-
envDir,
|
|
23
|
+
envDir: false,
|
|
20
24
|
server: { proxy },
|
|
21
25
|
preview: { proxy },
|
|
22
26
|
build: {
|
|
@@ -61,11 +61,33 @@ test('service rejects plaintext business requests', async ({ request }) => {
|
|
|
61
61
|
})
|
|
62
62
|
|
|
63
63
|
{{#if fullstack.e2eFrontend}}
|
|
64
|
+
test('hash routing restores direct links, refresh and browser history', async ({ page }) => {
|
|
65
|
+
await page.goto('/#/roles?source=direct')
|
|
66
|
+
await login(page, 'admin', adminPassword)
|
|
67
|
+
await expect(page.getByTestId('roles-page')).toBeVisible()
|
|
68
|
+
await expect(page).toHaveURL(/#\/roles\?source=direct$/)
|
|
69
|
+
await page.reload()
|
|
70
|
+
await expect(page.getByTestId('roles-page')).toBeVisible()
|
|
71
|
+
await expect(page.locator('header')).toBeVisible()
|
|
72
|
+
await expect(page.locator('footer')).toBeVisible()
|
|
73
|
+
await page.getByTestId('nav-settings').click()
|
|
74
|
+
await expect(page).toHaveURL(/#\/settings$/)
|
|
75
|
+
await page.goBack()
|
|
76
|
+
await expect(page.getByTestId('roles-page')).toBeVisible()
|
|
77
|
+
await page.goForward()
|
|
78
|
+
await expect(page.getByTestId('profile-form')).toBeVisible()
|
|
79
|
+
await page.goto('/#/unknown-page')
|
|
80
|
+
await expect(page).toHaveURL(/#\/users$/)
|
|
81
|
+
await expect(page.getByTestId('users-page')).toBeVisible()
|
|
82
|
+
})
|
|
83
|
+
|
|
64
84
|
test('branded authentication, profile layout and blur validation work across screen sizes', async ({ page }, testInfo) => {
|
|
65
85
|
test.setTimeout(60_000)
|
|
66
86
|
await page.setViewportSize({ width: 1440, height: 960 })
|
|
67
87
|
await page.goto('/')
|
|
68
88
|
await expect(page.getByRole('heading', { name: configuredName, exact: true })).toBeVisible()
|
|
89
|
+
await expect(page.locator('header')).toBeVisible()
|
|
90
|
+
await expect(page.locator('footer')).toBeVisible()
|
|
69
91
|
await capture(page, '{{projectName}}-login-desktop.png', testInfo.outputPath('login-desktop.png'))
|
|
70
92
|
await page.getByRole('button', { name: /立即注册|Register now/, exact: true }).click()
|
|
71
93
|
await expect(page.locator('.register-form')).toBeVisible()
|
|
@@ -74,6 +96,7 @@ test('branded authentication, profile layout and blur validation work across scr
|
|
|
74
96
|
await capture(page, '{{projectName}}-register-mobile.png', testInfo.outputPath('register-mobile.png'))
|
|
75
97
|
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
|
|
76
98
|
await page.reload()
|
|
99
|
+
await expect(page.getByTestId('login-form')).toBeVisible()
|
|
77
100
|
await capture(page, '{{projectName}}-login-mobile.png', testInfo.outputPath('login-mobile.png'))
|
|
78
101
|
await page.setViewportSize({ width: 1440, height: 960 })
|
|
79
102
|
await login(page, 'admin', adminPassword)
|
|
@@ -179,7 +202,7 @@ const additionalFrontends = [
|
|
|
179
202
|
{{#each fullstack.frontendApps}}
|
|
180
203
|
{
|
|
181
204
|
target: '{{target}}',
|
|
182
|
-
url: process.env
|
|
205
|
+
url: process.env['HAI_E2E_{{target}}_URL'.toUpperCase()] || 'http://127.0.0.1:{{port}}',
|
|
183
206
|
},
|
|
184
207
|
{{/each}}
|
|
185
208
|
].filter(frontend => frontend.target !== '{{fullstack.e2eFrontend.target}}')
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"images": [
|
|
3
|
+
{ "name": "app", "target": "runtime", "containerPort": 3000 }{{#if fullstack.frontends.web}},
|
|
4
|
+
{ "name": "web", "target": "web", "containerPort": 8080 }{{/if}}
|
|
5
|
+
]{{#if fullstack.frontends.desktop}},
|
|
6
|
+
"desktop": ["apps/{{projectName}}-desktop"]{{/if}}
|
|
7
|
+
}
|
|
@@ -10,20 +10,21 @@
|
|
|
10
10
|
"i18n:compile": "pnpm --filter {{fullstack.sharedPackageName}} paraglide:compile{{#each fullstack.frontendApps}} && pnpm --filter {{packageName}} paraglide:compile{{/each}}",
|
|
11
11
|
"postinstall": "pnpm i18n:compile",
|
|
12
12
|
"dev": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm --parallel --filter {{fullstack.servPackageName}}{{#each fullstack.frontendApps}} --filter {{packageName}}{{/each}} dev",
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"deploy": "pnpm package",
|
|
13
|
+
"start": "pnpm --filter {{fullstack.servPackageName}} start",
|
|
14
|
+
"start:test": "pnpm --filter {{fullstack.servPackageName}} start:test",
|
|
15
|
+
"start:prod": "pnpm --filter {{fullstack.servPackageName}} start:prod",
|
|
16
|
+
"dev:test": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm --parallel --filter {{fullstack.servPackageName}}{{#each fullstack.frontendApps}} --filter {{packageName}}{{/each}} dev:test",
|
|
17
|
+
"dev:prod": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm --parallel --filter {{fullstack.servPackageName}}{{#each fullstack.frontendApps}} --filter {{packageName}}{{/each}} dev:prod",
|
|
18
|
+
"compile": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm -r --if-present build",
|
|
19
|
+
"build": "hai build",
|
|
20
|
+
"deploy": "hai deploy packages/{{projectName}}-serv",
|
|
21
|
+
"check": "pnpm typecheck",
|
|
23
22
|
"typecheck": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm -r --if-present typecheck",
|
|
24
23
|
"lint": "eslint *.js *.ts e2e && pnpm -r --if-present lint",
|
|
24
|
+
"lint:fix": "eslint *.js *.ts e2e --fix && pnpm -r --if-present lint:fix",
|
|
25
25
|
"test": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm -r --if-present test",
|
|
26
|
-
"test:
|
|
26
|
+
"test:watch": "pnpm -r --parallel --if-present test:watch",
|
|
27
|
+
"test:e2e": "pnpm compile && node scripts/run-e2e.mjs",
|
|
27
28
|
"clean": "pnpm -r --if-present clean && rimraf node_modules .turbo"
|
|
28
29
|
},
|
|
29
30
|
"dependencies": {
|
|
@@ -32,6 +33,10 @@
|
|
|
32
33
|
"{{fullstack.sharedPackageName}}": "workspace:*"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
36
|
+
"@h-ai/cli": "{{haiPackageSpecifier}}",
|
|
37
|
+
{{#if features.deploy}}
|
|
38
|
+
"@h-ai/deploy": "{{haiPackageSpecifier}}",
|
|
39
|
+
{{/if}}
|
|
35
40
|
"@antfu/eslint-config": "{{fullstack.packageSpecifiers.antfuEslintConfig}}",
|
|
36
41
|
"@playwright/test": "{{fullstack.packageSpecifiers.playwright}}",
|
|
37
42
|
"@sveltejs/vite-plugin-svelte": "{{fullstack.packageSpecifiers.sveltePlugin}}",
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# {{fullstack.contractPackageName}}
|
|
2
2
|
|
|
3
|
-
共享 API 契约包,包含 Zod Schema、`apiContract.route()` 业务接口定义与工程级 contract
|
|
3
|
+
共享 API 契约包,包含 Zod Schema、`apiContract.route()` 业务接口定义与工程级 contract。业务 schema、角色、流程及限额是模板初始方案,按用户需求修改,并同步服务端授权、调用方、测试与说明;不要求需求沿用默认业务。
|
|
4
4
|
|
|
5
5
|
## 骨架说明
|
|
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
|
|
9
|
+
- `src/admin-schemas.ts` / `src/admin-contract.ts`:示例管理员用户、初始 admin/user 角色、分页审计、个人设置、加密 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 文件内重复使用时才保留私有常量。
|
|
@@ -14,8 +14,10 @@
|
|
|
14
14
|
"types": "./dist/index.d.ts",
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "tsup",
|
|
17
|
+
"check": "pnpm typecheck",
|
|
17
18
|
"typecheck": "tsc --noEmit",
|
|
18
19
|
"lint": "eslint .",
|
|
20
|
+
"lint:fix": "eslint . --fix",
|
|
19
21
|
"test": "vitest run",
|
|
20
22
|
"test:watch": "vitest",
|
|
21
23
|
"clean": "rimraf dist .turbo"
|
package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-contract.ts.hbs
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
ProfileUpdateSchema,
|
|
15
15
|
} from './admin-schemas.js'
|
|
16
16
|
|
|
17
|
-
/** mini
|
|
17
|
+
/** mini 管理示例的业务路径;初始 admin/user 角色可按实际需求调整。 */
|
|
18
18
|
export const adminContract = {
|
|
19
19
|
users: {
|
|
20
20
|
list: apiContract.route({ method: 'POST', path: '/admin/users/list', tags: ['admin'] })
|
package/templates/apps/fullstack/packages/{{projectName}}-contract/tests/contract.test.ts.hbs
CHANGED
|
@@ -1,36 +1,30 @@
|
|
|
1
1
|
import { apiContract } from '@h-ai/api-contract'
|
|
2
2
|
import { describe, expect, it } from 'vitest'
|
|
3
|
-
import
|
|
4
|
-
appContract,
|
|
5
|
-
AppEchoInputSchema,
|
|
6
|
-
AppInfoOutputDataSchema,
|
|
7
|
-
ENABLED_FRONTENDS,
|
|
8
|
-
{{fullstack.contractExportName}},
|
|
9
|
-
} from '../src/index.js'
|
|
3
|
+
import * as contracts from '../src/index.js'
|
|
10
4
|
|
|
11
5
|
describe('{{fullstack.contractPackageName}}', () => {
|
|
12
6
|
it('defines app contract routes', () => {
|
|
13
|
-
expect({{fullstack.contractExportName}}.app).toBeDefined()
|
|
14
|
-
expect(appContract.info).toBeDefined()
|
|
15
|
-
expect(apiContract.pathOf(appContract.info)).toBe('/app/info')
|
|
7
|
+
expect(contracts.{{fullstack.contractExportName}}.app).toBeDefined()
|
|
8
|
+
expect(contracts.appContract.info).toBeDefined()
|
|
9
|
+
expect(apiContract.pathOf(contracts.appContract.info)).toBe('/app/info')
|
|
16
10
|
})
|
|
17
11
|
|
|
18
12
|
it('validates app info payload', () => {
|
|
19
|
-
const result = AppInfoOutputDataSchema.safeParse({
|
|
13
|
+
const result = contracts.AppInfoOutputDataSchema.safeParse({
|
|
20
14
|
name: '{{fullstack.servPackageName}}',
|
|
21
15
|
version: '0.1.0',
|
|
22
16
|
env: 'test',
|
|
23
17
|
defaultLocale: 'zh-CN',
|
|
24
18
|
supportedLocales: ['zh-CN', 'en-US'],
|
|
25
19
|
startedAt: '2026-01-01T00:00:00.000Z',
|
|
26
|
-
frontends: [...ENABLED_FRONTENDS],
|
|
20
|
+
frontends: [...contracts.ENABLED_FRONTENDS],
|
|
27
21
|
})
|
|
28
22
|
|
|
29
23
|
expect(result.success).toBe(true)
|
|
30
24
|
})
|
|
31
25
|
|
|
32
26
|
it('validates echo input boundaries', () => {
|
|
33
|
-
expect(AppEchoInputSchema.safeParse({ message: 'hello' }).success).toBe(true)
|
|
34
|
-
expect(AppEchoInputSchema.safeParse({ message: '' }).success).toBe(false)
|
|
27
|
+
expect(contracts.AppEchoInputSchema.safeParse({ message: 'hello' }).success).toBe(true)
|
|
28
|
+
expect(contracts.AppEchoInputSchema.safeParse({ message: '' }).success).toBe(false)
|
|
35
29
|
})
|
|
36
30
|
})
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
## 默认功能与配置
|
|
6
6
|
|
|
7
|
+
以下是可运行示例的初始实现,角色/权限、管理流程、头像限额、分页、审计策略及部署方案均可按用户需求调整。需求与代码或模板规则不一致时,以需求为业务基线同步实现、测试与过期说明;实际框架 API 契约和服务端安全校验仍需满足。
|
|
8
|
+
|
|
7
9
|
- `config/_core.yml`:应用元信息(名称 / 版本 / 环境 / 语言),启动时由 `core.config` 加载。
|
|
8
10
|
- `config/_serv.yml`:服务端运行时设置(CORS 来源等)。
|
|
9
11
|
- `config/_db.yml` / `_cache.yml` / `_iam.yml` / `_storage.yml` / `_audit.yml` / `_reach.yml`:管理及开发通知配置,初始化前使用模块公开 Schema 验证。Reach 默认控制台 Provider,生产按注释替换为 SMTP/短信/API。
|
|
@@ -13,10 +15,10 @@
|
|
|
13
15
|
- `src/server/procedures/app-procedures.ts`:app 领域 procedures,对应 contract 中的 `app.info` / `app.echo` 路由。
|
|
14
16
|
- `src/server/procedures/admin-procedures.ts`:认证、管理员用户/角色/审计管理和个人设置,使用 `.auth()` / `.role('admin')` 守卫。每个请求读取用户最新启用状态、角色和权限。
|
|
15
17
|
- 标准认证 procedures 复用 `@h-ai/serv/features/iam`,注册、登录方式、重置与协议由 `_iam.yml` 控制;登录、退出、修改密码追加业务审计。HTTP App 类型复用公开 `ServHttpApp`。
|
|
16
|
-
- `src/server/admin-service.ts`:角色分配、头像上传和审计编排,全部底层操作复用 hai
|
|
18
|
+
- `src/server/admin-service.ts`:角色分配、头像上传和审计编排,全部底层操作复用 hai 模块。初始使用 admin/user 两种角色;锁内重新鉴权,默认禁止管理员自删、自禁用、自降级,并补偿用户更新失败后的角色变更。业务规则可按需求替换,保留的不变量需验证并发与失败路径。
|
|
17
19
|
- `tests/app-server.test.ts`:真实加密 HTTP 测试,覆盖 Cookie/受信原生刷新、RBAC、旧会话权限、并发管理操作、补偿、头像安全、偏好、审计及会话失效。
|
|
18
20
|
|
|
19
|
-
CLI 已生成根目录 `.env`,`HAI_ADMIN_DEFAULT_PASSWORD` 为每个项目独立的随机强密码;`dev`/`start` 自动读取,首次登录使用用户名 `admin` 和该密码。仅在没有用户时创建管理员,不重设已有用户密码。密码不符合 `_iam.yml` 策略时,启动日志显示具体原因及错误码。`.env` 被 Git 和容器构建忽略,`.env.example` 不含真实密码;部署时设置目标环境自己的密码。所有业务 JSON 请求和响应强制加密;健康检查和随机路径的只读头像图片可直接访问。头像支持 PNG/JPEG/WebP,最大 512 KiB,校验内容特征和大小后由服务端生成 key,不能指定任意文件路径。
|
|
21
|
+
CLI 已生成根目录 `.env.local`,`HAI_ADMIN_DEFAULT_PASSWORD` 为每个项目独立的随机强密码;`dev`/`start` 自动读取,首次登录使用用户名 `admin` 和该密码。仅在没有用户时创建管理员,不重设已有用户密码。密码不符合 `_iam.yml` 策略时,启动日志显示具体原因及错误码。`.env.local` 被 Git 和容器构建忽略,`.env.example` 不含真实密码;部署时设置目标环境自己的密码。所有业务 JSON 请求和响应强制加密;健康检查和随机路径的只读头像图片可直接访问。头像支持 PNG/JPEG/WebP,最大 512 KiB,校验内容特征和大小后由服务端生成 key,不能指定任意文件路径。
|
|
20
22
|
|
|
21
23
|
默认 SQLite + 本地 storage + memory cache 适合单实例,管理写入仅支持单写实例:进程内保护无过期时间,共享 cache 锁定期续租,丢失锁或释放失败会记录错误。多实例读取可使用共享数据库、Redis cache 和 S3 storage,但管理写请求必须统一路由到同一写实例;仅更换 Redis 不提供任意多写副本的最后管理员原子保证。另需配置传输会话粘性路由:服务端私钥驻留单个进程,服务重启后客户端需重新协商。Web 使用 HttpOnly refresh Cookie + 内存 access token;仅同时满足客户端标识、body 通道头与受信原生 Origin 的 App/Desktop 请求返回 refresh token body。
|
|
22
24
|
|
|
@@ -24,20 +26,20 @@ CLI 已生成根目录 `.env`,`HAI_ADMIN_DEFAULT_PASSWORD` 为每个项目独
|
|
|
24
26
|
|
|
25
27
|
## 开发
|
|
26
28
|
|
|
27
|
-
所有配置字段以模块 Schema 为准;未使用字段和替代 Provider 用注释保留。叶子环境变量只覆盖已有 YAML 路径,先取消相应键的注释,或用 `HAI_DB` / `HAI_CACHE` 等整体对象覆盖;`connectTimeout` 对应 `HAI_CACHE_CONNECTTIMEOUT`。凭据示例在根 `.env.example`,实际密钥保存在忽略的 `.env` 或部署环境。`.gitignore` 的 `**/data/` 忽略所有运行数据及 `.gitkeep`;运行时会创建目录。`crypto.init()` 无配置参数,datapipe 为纯函数,不提供虚构配置。
|
|
29
|
+
所有配置字段以模块 Schema 为准;未使用字段和替代 Provider 用注释保留。叶子环境变量只覆盖已有 YAML 路径,先取消相应键的注释,或用 `HAI_DB` / `HAI_CACHE` 等整体对象覆盖;`connectTimeout` 对应 `HAI_CACHE_CONNECTTIMEOUT`。凭据示例在根 `.env.example`,实际密钥保存在忽略的 `.env.local` 或部署环境。`.gitignore` 的 `**/data/` 忽略所有运行数据及 `.gitkeep`;运行时会创建目录。`crypto.init()` 无配置参数,datapipe 为纯函数,不提供虚构配置。
|
|
28
30
|
|
|
29
|
-
自动生成的 `.env`
|
|
31
|
+
自动生成的 `.env` 包含已启用模块的环境参数;YAML 显式引用相同变量并保留本地回退。使用 `start:test` / `start:prod` 加载对应环境覆盖。
|
|
30
32
|
|
|
31
33
|
- `pnpm --filter {{fullstack.servPackageName}} dev`
|
|
32
34
|
- `pnpm --filter {{fullstack.servPackageName}} test`
|
|
33
35
|
|
|
34
36
|
## 容器运行
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
从工程根运行 `pnpm build --target docker` 构建并导出镜像。启用 deploy 功能后,运行根命令 `pnpm deploy --env test`;含 Web 镜像时一并发布 API/Web,部署不重新构建。网络不通时用 `pnpm deploy --env prod --bundle-only`,按说明手工搬运 tar。
|
|
39
|
+
|
|
37
40
|
- 服务镜像直接由 `@h-ai/serv` 对外监听,配置挂载点为 `/app/config`,数据目录为 `/app/data`,不包含额外转发进程。
|
|
38
41
|
- Web 镜像不烘焙 API 地址;启动时使用 `-e PUBLIC_API_BASE=https://api.example.com` 注入,同一镜像可部署到不同环境。未设置时 Web 默认请求自身 origin。
|
|
39
42
|
- YAML 配置使用 `${ENV_NAME:default}` 接收运行时环境变量;不要把密钥写入镜像。
|
|
40
|
-
- 将 SQLite/本地存储放入本目录 `data/` 后运行 `pnpm docker:build:data` / `pnpm podman:build:data`,可生成带初始数据的镜像,持续写入仍应使用 volume。
|
|
41
43
|
|
|
42
44
|
## IAM 认证配置
|
|
43
45
|
|
|
@@ -7,17 +7,17 @@ providers:
|
|
|
7
7
|
# SMTP 替换 email 控制台 Provider;同名 Provider 只保留一个。
|
|
8
8
|
# - name: email
|
|
9
9
|
# type: smtp
|
|
10
|
-
# host: ${
|
|
11
|
-
# port: ${
|
|
10
|
+
# host: ${HAI_REACH_PROVIDERS_0_HOST:smtp.example.com}
|
|
11
|
+
# port: ${HAI_REACH_PROVIDERS_0_PORT:465}
|
|
12
12
|
# secure: true
|
|
13
|
-
# user: ${
|
|
14
|
-
# pass: ${
|
|
15
|
-
# from: ${
|
|
13
|
+
# user: ${HAI_REACH_PROVIDERS_0_USER:}
|
|
14
|
+
# pass: ${HAI_REACH_PROVIDERS_0_PASS:}
|
|
15
|
+
# from: ${HAI_REACH_PROVIDERS_0_FROM:noreply@example.com}
|
|
16
16
|
# 阿里云短信替换 sms 控制台 Provider。
|
|
17
17
|
# - name: sms
|
|
18
18
|
# type: aliyun-sms
|
|
19
|
-
# accessKeyId: ${
|
|
20
|
-
# accessKeySecret: ${
|
|
19
|
+
# accessKeyId: ${HAI_REACH_PROVIDERS_0_ACCESSKEYID}
|
|
20
|
+
# accessKeySecret: ${HAI_REACH_PROVIDERS_0_ACCESSKEYSECRET}
|
|
21
21
|
# signName: MyApp
|
|
22
22
|
# endpoint: dysmsapi.aliyuncs.com
|
|
23
23
|
# 自有发送服务:按需将 template.provider 改为 custom。
|
|
@@ -26,7 +26,7 @@ providers:
|
|
|
26
26
|
# url: https://notify.example.com/send
|
|
27
27
|
# method: POST # POST | PUT
|
|
28
28
|
# headers:
|
|
29
|
-
# Authorization: '
|
|
29
|
+
# Authorization: '${HAI_REACH_PROVIDERS_0_HEADERS_AUTHORIZATION}'
|
|
30
30
|
# timeout: 10000
|
|
31
31
|
templates:
|
|
32
32
|
- name: otp_email
|
|
@@ -30,9 +30,9 @@ http:
|
|
|
30
30
|
# gatewaySecret: ${HAI_SERV_HTTP_RPC_GATEWAYSECRET}
|
|
31
31
|
|
|
32
32
|
cors:
|
|
33
|
-
origin: ${
|
|
34
|
-
nativeOrigins: ${
|
|
35
|
-
credentials: ${
|
|
33
|
+
origin: ${HAI_SERV_CORS_ORIGIN:*}
|
|
34
|
+
nativeOrigins: ${HAI_SERV_CORS_NATIVEORIGINS:http://localhost,https://localhost,http://tauri.localhost,https://tauri.localhost,tauri://localhost,capacitor://localhost}
|
|
35
|
+
credentials: ${HAI_SERV_CORS_CREDENTIALS:true}
|
|
36
36
|
allowedHeaders:
|
|
37
37
|
- Authorization
|
|
38
38
|
- Content-Type
|
|
@@ -20,13 +20,19 @@
|
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
22
|
"build": "tsup",
|
|
23
|
+
"check": "pnpm typecheck",
|
|
23
24
|
"typecheck": "tsc --noEmit",
|
|
24
25
|
"lint": "eslint .",
|
|
26
|
+
"lint:fix": "eslint . --fix",
|
|
25
27
|
"test": "vitest run",
|
|
26
28
|
"test:watch": "vitest",
|
|
27
29
|
"clean": "rimraf dist .turbo",
|
|
28
|
-
"dev": "pnpm build && node --env-file-if-exists=../../.env dist/index.js",
|
|
29
|
-
"start": "node --env-file-if-exists=../../.env dist/index.js"
|
|
30
|
+
"dev": "pnpm build && node --env-file=../../.env --env-file=../../.env.dev --env-file-if-exists=../../.env.local dist/index.js",
|
|
31
|
+
"start": "node --env-file=../../.env --env-file=../../.env.prod --env-file-if-exists=../../.env.local dist/index.js",
|
|
32
|
+
"dev:test": "pnpm build && node --env-file=../../.env --env-file=../../.env.test --env-file-if-exists=../../.env.local dist/index.js",
|
|
33
|
+
"dev:prod": "pnpm build && pnpm start",
|
|
34
|
+
"start:test": "node --env-file=../../.env --env-file=../../.env.test --env-file-if-exists=../../.env.local dist/index.js",
|
|
35
|
+
"start:prod": "pnpm start"
|
|
30
36
|
},
|
|
31
37
|
"dependencies": {
|
|
32
38
|
"@h-ai/audit": "{{haiPackageSpecifier}}",
|
|
@@ -6,7 +6,7 @@ import { iam } from '@h-ai/iam'
|
|
|
6
6
|
import { reldb } from '@h-ai/reldb'
|
|
7
7
|
import { serv, ServConfigSchema } from '@h-ai/serv'
|
|
8
8
|
import { storage } from '@h-ai/storage'
|
|
9
|
-
import
|
|
9
|
+
import * as contracts from '{{fullstack.contractPackageName}}'
|
|
10
10
|
import { createServerProcedures } from './server/procedures/index.js'
|
|
11
11
|
|
|
12
12
|
const STARTED_AT = new Date().toISOString()
|
|
@@ -27,16 +27,16 @@ export function createServerApp(): ServHttpApp {
|
|
|
27
27
|
]
|
|
28
28
|
|
|
29
29
|
return serv.createApp({
|
|
30
|
-
contract: {{fullstack.contractExportName}},
|
|
30
|
+
contract: contracts.{{fullstack.contractExportName}},
|
|
31
31
|
procedures: createServerProcedures({
|
|
32
32
|
app: {
|
|
33
33
|
name: coreConfig.name,
|
|
34
34
|
version: coreConfig.version,
|
|
35
35
|
env: coreConfig.env,
|
|
36
36
|
defaultLocale: coreConfig.defaultLocale,
|
|
37
|
-
supportedLocales: SUPPORTED_LOCALES,
|
|
37
|
+
supportedLocales: contracts.SUPPORTED_LOCALES,
|
|
38
38
|
startedAt: STARTED_AT,
|
|
39
|
-
frontends: [...ENABLED_FRONTENDS],
|
|
39
|
+
frontends: [...contracts.ENABLED_FRONTENDS],
|
|
40
40
|
},
|
|
41
41
|
}),
|
|
42
42
|
iam,
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Shared mini administration workspace for **{{projectName}}**. Web, Capacitor App and Tauri desktop use the same `AdminWorkspace`, responsive `AppShell`, typed API client and management store.
|
|
4
4
|
|
|
5
|
+
The behavior below describes the initial example. Roles, permissions, workflows, limits, preferences and audit policies can change with the user's requirements. Update conflicting implementation, tests and guidance together; preserve the applicable public API contracts and server security checks.
|
|
6
|
+
|
|
5
7
|
- Administrators manage users, upload user avatars, assign the built-in administrator/user roles, edit role descriptions and query audit logs.
|
|
6
|
-
-
|
|
8
|
+
- By default, ordinary users update their own profile, avatar, language, appearance and password. The server enforces the actual business permissions on every request.
|
|
7
9
|
- `ManagementStore` owns API calls and business state; components render the state and forward user interactions.
|
|
8
10
|
- Application name, version, environment and default locale come from the backend `app.info` contract, which reads `core` configuration. Do not duplicate these values in translations or frontend constants.
|
|
9
11
|
- Requests, token refresh and JSON responses use the same `@h-ai/api-client` + `@h-ai/crypto` transport. Avatar uploads use encrypted JSON and validate PNG/JPEG/WebP content, with a 512 KiB limit.
|
|
@@ -37,7 +39,7 @@ The scaffold theme configuration determines the themes compiled into frontend CS
|
|
|
37
39
|
|
|
38
40
|
## Validation
|
|
39
41
|
|
|
40
|
-
Run root `pnpm typecheck`, `pnpm lint`, `pnpm build`, `pnpm test` and `pnpm test:e2e`. Shared API tests exercise real transport key exchange, encrypted request/response handling, configured core metadata and credential forwarding. Root browser tests cover administrator and ordinary-user workflows, avatar rendering, persisted preferences, password/session invalidation and narrow-screen layout. All selected Web/App/Desktop previews also exercise login, tours, help and profile navigation.
|
|
42
|
+
Run root `pnpm typecheck`, `pnpm lint`, `pnpm build`, `pnpm test` and `pnpm test:e2e`. Shared API tests exercise real transport key exchange, encrypted request/response handling, configured core metadata and credential forwarding. Root browser tests cover administrator and ordinary-user workflows, avatar rendering, persisted preferences, password/session invalidation and narrow-screen layout. All selected Web/App/Desktop previews also exercise login, tours, help and profile navigation. Clean screenshots, reports, temporary projects, containers and helper processes in finally/teardown, including failure paths. These browser checks do not replace native-shell or miniapp device acceptance.
|
|
41
43
|
|
|
42
44
|
## IAM configuration
|
|
43
45
|
|
|
@@ -24,8 +24,10 @@
|
|
|
24
24
|
"paraglide:compile": "paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide",
|
|
25
25
|
"paraglide:watch": "paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide --watch",
|
|
26
26
|
"prepare": "paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide",
|
|
27
|
+
"check": "pnpm typecheck",
|
|
27
28
|
"typecheck": "pnpm paraglide:compile && svelte-check --tsconfig ./tsconfig.json",
|
|
28
29
|
"lint": "eslint .",
|
|
30
|
+
"lint:fix": "eslint . --fix",
|
|
29
31
|
"test": "vitest run --passWithNoTests",
|
|
30
32
|
"clean": "rimraf .turbo src/lib/paraglide"
|
|
31
33
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang='ts'>
|
|
2
|
+
import type { Component } from 'svelte'
|
|
2
3
|
import type { ManagementApiOptions } from '../api/api-client.js'
|
|
3
4
|
import { Alert, Skeleton } from '@h-ai/ui/components/compounds/index'
|
|
4
5
|
import { Button } from '@h-ai/ui/components/primitives/index'
|
|
@@ -6,9 +7,10 @@
|
|
|
6
7
|
import { GuideHost } from '@h-ai/ui/guide'
|
|
7
8
|
import { onMount, untrack } from 'svelte'
|
|
8
9
|
import { managementAuthActions } from '../api/api-client.js'
|
|
10
|
+
import { HashRouter } from '../navigation/hash-router.svelte.js'
|
|
9
11
|
import * as m from '../paraglide/messages.js'
|
|
10
|
-
import { ManagementStore } from '../stores/management-store.svelte.js'
|
|
11
12
|
import { createManagementGuide } from '../stores/guide-store.js'
|
|
13
|
+
import { ManagementStore } from '../stores/management-store.svelte.js'
|
|
12
14
|
import AppShell from './AppShell.svelte'
|
|
13
15
|
|
|
14
16
|
interface Props {
|
|
@@ -17,7 +19,9 @@
|
|
|
17
19
|
onLocaleChange?: (locale: string) => void
|
|
18
20
|
}
|
|
19
21
|
const { platform, apiOptions = {}, onLocaleChange }: Props = $props()
|
|
20
|
-
const
|
|
22
|
+
const pages = import.meta.glob<{ default: Component<{ store: ManagementStore }> }>('../pages/**/*.svelte')
|
|
23
|
+
const router = new HashRouter(Object.keys(pages), '/users')
|
|
24
|
+
const store = new ManagementStore(router)
|
|
21
25
|
// 宿主平台在组件生命周期中固定,引导控制器只创建一次。
|
|
22
26
|
const guide = untrack(() => createManagementGuide(store, platform))
|
|
23
27
|
let resetToken = $state('')
|
|
@@ -34,19 +38,16 @@
|
|
|
34
38
|
}
|
|
35
39
|
resetToken = ''
|
|
36
40
|
}
|
|
37
|
-
const pageComponent = $derived.
|
|
38
|
-
if (store.isAdmin && store.page === 'users')
|
|
39
|
-
return import('./UserManagement.svelte')
|
|
40
|
-
if (store.isAdmin && store.page === 'roles')
|
|
41
|
-
return import('./RoleManagement.svelte')
|
|
42
|
-
if (store.isAdmin && store.page === 'audit')
|
|
43
|
-
return import('./AuditManagement.svelte')
|
|
44
|
-
return import('./ProfileSettings.svelte')
|
|
45
|
-
})
|
|
41
|
+
const pageComponent = $derived(pages[router.file]())
|
|
46
42
|
onMount(() => {
|
|
43
|
+
const stopRouting = router.start(() => {
|
|
44
|
+
if (store.user)
|
|
45
|
+
void store.navigate(router.path.slice(1))
|
|
46
|
+
})
|
|
47
47
|
resetToken = new URL(location.href).searchParams.get('token') ?? ''
|
|
48
48
|
void store.initialize(apiOptions, onLocaleChange)
|
|
49
49
|
return () => {
|
|
50
|
+
stopRouting()
|
|
50
51
|
void store.close()
|
|
51
52
|
}
|
|
52
53
|
})
|
|
@@ -60,6 +61,7 @@
|
|
|
60
61
|
{#if store.initializing}
|
|
61
62
|
<div class='grid min-h-dvh place-items-center bg-base-200/50'><div class='w-72 space-y-4'><Skeleton variant='title' /><Skeleton count={3} /><p class='text-center text-sm text-base-content/50'>{m.home_loading()}</p></div></div>
|
|
62
63
|
{:else if !store.user || resetToken}
|
|
64
|
+
<header class='border-b border-base-content/8 bg-base-100 px-6 py-3 text-sm font-medium'>{store.info?.name}</header>
|
|
63
65
|
<main data-testid='login-page'>
|
|
64
66
|
{#if !store.info || !store.authConfig}
|
|
65
67
|
<section class='mx-auto mt-20 max-w-md space-y-4 p-6'>
|
|
@@ -71,7 +73,7 @@
|
|
|
71
73
|
{#if store.authConfig && store.error}<Alert variant='error' class='mx-auto max-w-lg'>{store.error}</Alert>{/if}
|
|
72
74
|
<AuthPanel config={authConfig} actions={managementAuthActions} {resetToken} initialMode={resetToken ? 'reset' : 'login'} error={store.authConfig ? '' : store.error} onretry={() => void store.initialize(apiOptions, onLocaleChange)} onauthenticated={authenticated} />
|
|
73
75
|
</div>
|
|
74
|
-
<
|
|
76
|
+
<footer class='border-t border-base-content/8 p-3 text-center text-xs text-base-content/50'><span data-testid='app-name'>{store.info.name}</span> · <span data-testid='app-version'>v{store.info.version}</span> · <span data-testid='app-environment'>{store.info.env}</span></footer>
|
|
75
77
|
{/if}
|
|
76
78
|
</main>
|
|
77
79
|
{:else}
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
: []),
|
|
23
23
|
{ id: 'settings' as const, label: m.settings_title(), icon: 'icon-[tabler--settings]' },
|
|
24
24
|
])
|
|
25
|
-
const title = $derived(items.find(item => item.id === store.page)?.label ??
|
|
25
|
+
const title = $derived(items.find(item => item.id === store.page)?.label ?? store.info?.name ?? '')
|
|
26
26
|
|
|
27
27
|
function navigate(page: ManagementPage) {
|
|
28
28
|
menuOpen = false
|
|
@@ -3,6 +3,7 @@ import type { HaiResult } from '@h-ai/core'
|
|
|
3
3
|
import type { ChangePasswordFormData, UserProfileSubmitData } from '@h-ai/ui'
|
|
4
4
|
import type { AdminAuditLog, AdminCreateUser, AdminRole, AdminUpdateUser, AdminUser, AvatarUpload, ProfileUpdate } from '{{fullstack.contractPackageName}}'
|
|
5
5
|
import type { AppInfo, ManagementApiOptions } from '../api/api-client.js'
|
|
6
|
+
import type { HashRouter } from '../navigation/hash-router.svelte.js'
|
|
6
7
|
import { HaiApiClientError } from '@h-ai/api-client'
|
|
7
8
|
import { setGlobalLocale } from '@h-ai/ui/i18n'
|
|
8
9
|
import { SvelteDate } from 'svelte/reactivity'
|
|
@@ -13,10 +14,20 @@ import { AVAILABLE_THEMES, DEFAULT_THEME, themeStore } from './theme-store.svelt
|
|
|
13
14
|
|
|
14
15
|
type ResultData<T> = T extends { success: true, data: infer D } ? D : never
|
|
15
16
|
export type CurrentUser = ResultData<Awaited<ReturnType<typeof managementApi.iam.auth.currentUser>>>
|
|
16
|
-
|
|
17
|
+
/** 页面目录对应的相对路径;例如 `reports/weekly`,新增页面无需修改类型或路由表。 */
|
|
18
|
+
export type ManagementPage = string
|
|
17
19
|
|
|
18
20
|
/** 页面状态和业务操作在共享 store 中收敛,宿主端仅装配平台能力。 */
|
|
19
21
|
export class ManagementStore {
|
|
22
|
+
/**
|
|
23
|
+
* 绑定当前工作区的页面路由;服务端继续验证各 API 的权限。
|
|
24
|
+
* @example new ManagementStore(new HashRouter(Object.keys(pages), '/users'))
|
|
25
|
+
*/
|
|
26
|
+
constructor(router: HashRouter) {
|
|
27
|
+
this.router = router
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private readonly router: HashRouter
|
|
20
31
|
info = $state<AppInfo | null>(null)
|
|
21
32
|
authConfig = $state<IamPublicConfig | null>(null)
|
|
22
33
|
user = $state<CurrentUser | null>(null)
|
|
@@ -76,7 +87,7 @@ export class ManagementStore {
|
|
|
76
87
|
const me = await managementApi.iam.auth.currentUser()
|
|
77
88
|
if (me.success) {
|
|
78
89
|
this.setUser(me.data)
|
|
79
|
-
await this.navigate(this.
|
|
90
|
+
await this.navigate(this.router.path.slice(1))
|
|
80
91
|
}
|
|
81
92
|
else if (me.error.httpStatus !== 401) {
|
|
82
93
|
this.error = m.operation_failed()
|
|
@@ -102,7 +113,7 @@ export class ManagementStore {
|
|
|
102
113
|
if (!me?.success)
|
|
103
114
|
return
|
|
104
115
|
this.setUser(me.data)
|
|
105
|
-
await this.navigate(this.
|
|
116
|
+
await this.navigate(this.router.path.slice(1))
|
|
106
117
|
}
|
|
107
118
|
|
|
108
119
|
async logout(): Promise<void> {
|
|
@@ -121,6 +132,9 @@ export class ManagementStore {
|
|
|
121
132
|
this.requestVersion++
|
|
122
133
|
this.loading = false
|
|
123
134
|
this.page = this.isAdmin ? page : 'settings'
|
|
135
|
+
if (this.router.path !== `/${this.page}`)
|
|
136
|
+
this.router.navigate(`/${this.page}`)
|
|
137
|
+
this.page = this.router.path.slice(1)
|
|
124
138
|
this.error = ''
|
|
125
139
|
this.notice = ''
|
|
126
140
|
if (this.page === 'users')
|