@h-ai/cli 0.1.0-alpha.57 → 0.1.0-alpha.59
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 +70 -21
- package/README.ja.md +70 -21
- package/README.md +70 -21
- package/dist/chunk-2H7UOFLK.js +0 -0
- package/dist/{chunk-O3ANVICQ.js → chunk-WUSFJFW5.js} +13 -7
- package/dist/chunk-WUSFJFW5.js.map +1 -0
- package/dist/{chunk-MJGUBOAV.js → chunk-WWG3U5ZD.js} +24 -42
- package/dist/chunk-WWG3U5ZD.js.map +1 -0
- package/dist/cli-config-templates-6XA7V6CC.js +5 -0
- package/dist/{cli-config-templates-OWBEGBZQ.js.map → cli-config-templates-6XA7V6CC.js.map} +1 -1
- package/dist/{cli-skill-templates-ZYXES3NT.js → cli-skill-templates-2KDUACT6.js} +3 -3
- package/dist/{cli-skill-templates-ZYXES3NT.js.map → cli-skill-templates-2KDUACT6.js.map} +1 -1
- package/dist/index.js +192 -125
- package/dist/index.js.map +1 -1
- package/dist/{src-TBIET7YU.js → src-DUGLX4YB.js} +7270 -2119
- package/dist/src-DUGLX4YB.js.map +1 -0
- package/package.json +19 -17
- package/templates/README.md +4 -4
- package/templates/apps/admin/src/routes/+layout.svelte.hbs +7 -0
- package/templates/apps/admin/src/routes/+page.svelte.hbs +5 -4
- package/templates/apps/admin/src/routes/admin/+layout.svelte.hbs +2 -2
- package/templates/apps/api/.npmrc.hbs +1 -2
- package/templates/apps/api/Dockerfile.hbs +6 -5
- package/templates/apps/api/apps/{{projectName}}-contract/package.json.hbs +1 -1
- package/templates/apps/api/apps/{{projectName}}-contract/src/app-schemas.ts.hbs +2 -0
- package/templates/apps/api/apps/{{projectName}}-contract/tsup.config.ts.hbs +1 -1
- package/templates/apps/api/apps/{{projectName}}-service/config/_core.yml.hbs +8 -0
- package/templates/apps/api/apps/{{projectName}}-service/package.json.hbs +1 -1
- package/templates/apps/api/apps/{{projectName}}-service/src/app.ts.hbs +7 -8
- package/templates/apps/api/apps/{{projectName}}-service/src/index.ts.hbs +4 -3
- package/templates/apps/api/apps/{{projectName}}-service/src/lib/server/init.ts.hbs +0 -1
- package/templates/apps/api/apps/{{projectName}}-service/src/server/procedures/app-procedures.ts.hbs +4 -0
- package/templates/apps/api/apps/{{projectName}}-service/tests/service.test.ts.hbs +3 -1
- package/templates/apps/api/e2e/api-service.spec.ts.hbs +3 -1
- package/templates/apps/api/package.json.hbs +2 -2
- package/templates/apps/api/pnpm-workspace.yaml.hbs +26 -0
- package/templates/apps/api/tsconfig.base.json +0 -1
- package/templates/apps/fullstack/.dockerignore.hbs +3 -0
- package/templates/apps/fullstack/.npmrc.hbs +1 -2
- package/templates/apps/fullstack/Dockerfile.hbs +6 -5
- package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +2 -2
- package/templates/apps/fullstack/apps/{{projectName}}-app/capacitor.config.ts.hbs +1 -1
- package/templates/apps/fullstack/apps/{{projectName}}-app/index.html.hbs +7 -2
- package/templates/apps/fullstack/apps/{{projectName}}-app/messages/en-US.json.hbs +1 -34
- package/templates/apps/fullstack/apps/{{projectName}}-app/messages/zh-CN.json.hbs +1 -34
- package/templates/apps/fullstack/apps/{{projectName}}-app/package.json.hbs +2 -2
- package/templates/apps/fullstack/apps/{{projectName}}-app/src/App.svelte.hbs +30 -170
- package/templates/apps/fullstack/apps/{{projectName}}-app/src/app.d.ts.hbs +1 -1
- package/templates/apps/fullstack/apps/{{projectName}}-app/src/lib/capacitor.ts.hbs +6 -5
- package/templates/apps/fullstack/apps/{{projectName}}-app/src/main.ts.hbs +1 -1
- package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +48 -22
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +3 -1
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/index.html.hbs +6 -1
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/messages/en-US.json.hbs +1 -28
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/messages/zh-CN.json.hbs +1 -28
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/package.json.hbs +5 -4
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/App.svelte.hbs +10 -141
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/Cargo.toml.hbs +5 -5
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/tauri.conf.json.hbs +3 -3
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +21 -2
- package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +2 -2
- package/templates/apps/fullstack/apps/{{projectName}}-web/index.html.hbs +6 -1
- package/templates/apps/fullstack/apps/{{projectName}}-web/messages/en-US.json.hbs +1 -28
- package/templates/apps/fullstack/apps/{{projectName}}-web/messages/zh-CN.json.hbs +1 -28
- package/templates/apps/fullstack/apps/{{projectName}}-web/package.json.hbs +2 -2
- package/templates/apps/fullstack/apps/{{projectName}}-web/src/App.svelte.hbs +5 -143
- package/templates/apps/fullstack/apps/{{projectName}}-web/src/app.d.ts.hbs +1 -1
- package/templates/apps/fullstack/apps/{{projectName}}-web/src/main.ts.hbs +1 -1
- package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +48 -22
- package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +206 -16
- package/templates/apps/fullstack/package.json.hbs +2 -2
- package/templates/apps/fullstack/packages/{{projectName}}-contract/README.md.hbs +2 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/package.json.hbs +1 -1
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-contract.ts.hbs +66 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-schemas.ts.hbs +96 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/app-schemas.ts.hbs +3 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/index.ts.hbs +3 -1
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +5 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/tests/contract.test.ts.hbs +3 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/tsup.config.ts.hbs +1 -1
- package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +13 -3
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_audit.yml.hbs +3 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_cache.yml.hbs +2 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_core.yml.hbs +10 -3
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_db.yml.hbs +3 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_iam.yml.hbs +21 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +10 -3
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_storage.yml.hbs +3 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +10 -3
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/app-server.ts.hbs +63 -28
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/index.ts.hbs +26 -11
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/admin-service.ts.hbs +276 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/messages.ts.hbs +29 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/admin-procedures.ts.hbs +94 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/app-procedures.ts.hbs +6 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/index.ts.hbs +5 -1
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/runtime.ts.hbs +98 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/app-server.test.ts.hbs +446 -33
- package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/runtime.test.ts.hbs +48 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/tsup.config.ts.hbs +1 -1
- package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +19 -26
- package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/en-US.json.hbs +78 -15
- package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/zh-CN.json.hbs +78 -15
- package/templates/apps/fullstack/packages/{{projectName}}-shared/package.json.hbs +4 -1
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/index.ts.hbs +3 -4
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/api-client.ts.hbs +53 -27
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AdminWorkspace.svelte.hbs +73 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +55 -45
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AuditManagement.svelte.hbs +61 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/ProfileSettings.svelte.hbs +57 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/RoleManagement.svelte.hbs +49 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/UserManagement.svelte.hbs +174 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/management-store.svelte.ts.hbs +399 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/theme-store.svelte.ts.hbs +7 -39
- package/templates/apps/fullstack/packages/{{projectName}}-shared/svelte.config.js.hbs +1 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/api-client.test.ts.hbs +151 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/management-store.test.ts.hbs +101 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/vitest.config.ts.hbs +7 -0
- package/templates/apps/fullstack/playwright.config.ts.hbs +6 -0
- package/templates/apps/fullstack/pnpm-workspace.yaml.hbs +24 -0
- package/templates/apps/fullstack/tsconfig.base.json +0 -1
- package/templates/apps/h5/src/routes/+layout.svelte.hbs +4 -0
- package/templates/apps/h5/src/routes/+page.svelte.hbs +3 -2
- package/templates/apps/mobile-app/index.html.hbs +2 -2
- package/templates/apps/mobile-app/src/App.svelte.hbs +6 -2
- package/templates/apps/mobile-app/src/lib/app-config.ts.hbs +13 -0
- package/templates/apps/website/src/routes/+layout.svelte.hbs +8 -2
- package/templates/apps/website/src/routes/+page.svelte.hbs +4 -3
- package/templates/base/.npmrc.hbs +1 -15
- package/templates/base/Dockerfile.hbs +9 -8
- package/templates/base/e2e/app.spec.ts.hbs +1 -0
- package/templates/base/package.json.hbs +39 -42
- package/templates/base/pnpm-workspace.yaml.hbs +26 -0
- package/templates/base/src/app.d.ts.hbs +4 -0
- package/templates/base/src/hooks.server.ts.hbs +0 -8
- package/templates/base/src/lib/server/init.ts.hbs +26 -5
- package/templates/base/src/routes/+layout.server.ts.hbs +18 -0
- package/templates/base/tests/scaffold.test.ts.hbs +21 -1
- package/templates/base/vite.config.ts.hbs +21 -0
- package/templates/features/ai/routes/api/chat/+server.ts +6 -7
- package/templates/features/iam/routes-admin/(auth)/auth/login/+page.svelte.hbs +1 -1
- package/templates/features/iam/routes-h5/auth/login/+page.svelte.hbs +1 -1
- package/templates/features/iam/routes-shared/api/auth/login/+server.ts +14 -10
- package/templates/features/iam/routes-shared/api/auth/logout/+server.ts +6 -4
- package/templates/features/storage/routes/api/upload/+server.ts +10 -8
- package/templates/skills/CLAUDE.md +0 -14
- package/templates/skills/bridges/fullstack/AGENTS.md.hbs +13 -2
- package/templates/skills/bridges/fullstack/copilot-instructions.md.hbs +3 -0
- package/templates/skills/hai-api-client/SKILL.md +1 -1
- package/templates/skills/hai-app-create/SKILL.md +2 -0
- package/templates/skills/hai-app-create/development.md +21 -0
- package/templates/skills/hai-build/reference.md +4 -2
- package/templates/skills/hai-core/SKILL.md +2 -0
- package/templates/skills/hai-core/reference.md +6 -4
- package/templates/skills/hai-deploy/SKILL.md +2 -0
- package/templates/skills/hai-framework-sync/SKILL.md +5 -2
- package/templates/skills/hai-serv/SKILL.md +1 -0
- package/templates/skills/hai-serv/reference.md +3 -1
- package/templates/skills/hai-ui/SKILL.md +6 -0
- package/templates/skills/hai-ui/reference.md +7 -4
- package/templates/skills/hai-vecdb/reference.md +53 -49
- package/dist/chunk-MJGUBOAV.js.map +0 -1
- package/dist/chunk-O3ANVICQ.js.map +0 -1
- package/dist/cli-config-templates-OWBEGBZQ.js +0 -5
- package/dist/src-TBIET7YU.js.map +0 -1
- package/templates/apps/fullstack/apps/{{projectName}}-app/tests/api.test.ts.hbs +0 -28
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/tests/api.test.ts.hbs +0 -28
- package/templates/apps/fullstack/apps/{{projectName}}-web/tests/api.test.ts.hbs +0 -55
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/LanguageSwitcher.svelte.hbs +0 -27
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/ThemeSwitcher.svelte.hbs +0 -18
- package/templates/skills/bridges/admin/CLAUDE.md +0 -15
- package/templates/skills/bridges/api/CLAUDE.md +0 -15
- package/templates/skills/bridges/fullstack/CLAUDE.md.hbs +0 -9
- package/templates/skills/bridges/h5/CLAUDE.md +0 -15
- package/templates/skills/bridges/mobile-app/CLAUDE.md +0 -15
- package/templates/skills/bridges/website/CLAUDE.md +0 -15
- package/templates/skills/claude-skill.md.hbs +0 -7
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import type { HaiResult } from '@h-ai/core'
|
|
2
|
+
import type { ChangePasswordFormData, LoginFormData, UserProfileSubmitData } from '@h-ai/ui'
|
|
3
|
+
import type { AdminAuditLog, AdminCreateUser, AdminRole, AdminUpdateUser, AdminUser, AvatarUpload, ProfileUpdate } from '{{fullstack.contractPackageName}}'
|
|
4
|
+
import type { AppInfo, ManagementApiOptions } from '../api/api-client.js'
|
|
5
|
+
import { HaiApiClientError } from '@h-ai/api-client'
|
|
6
|
+
import { core } from '@h-ai/core'
|
|
7
|
+
import { SvelteDate } from 'svelte/reactivity'
|
|
8
|
+
import { initializeManagementApi, managementApi, resolveAvatarUrl } from '../api/api-client.js'
|
|
9
|
+
import * as m from '../paraglide/messages.js'
|
|
10
|
+
import { getLocale, locales, setLocale } from '../paraglide/runtime.js'
|
|
11
|
+
import { AVAILABLE_THEMES, DEFAULT_THEME, themeStore } from './theme-store.svelte.js'
|
|
12
|
+
|
|
13
|
+
type ResultData<T> = T extends { success: true, data: infer D } ? D : never
|
|
14
|
+
export type CurrentUser = ResultData<Awaited<ReturnType<typeof managementApi.iam.auth.currentUser>>>
|
|
15
|
+
export type ManagementPage = 'users' | 'roles' | 'audit' | 'settings'
|
|
16
|
+
|
|
17
|
+
/** 页面状态和业务操作在共享 store 中收敛,宿主端仅装配平台能力。 */
|
|
18
|
+
export class ManagementStore {
|
|
19
|
+
info = $state<AppInfo | null>(null)
|
|
20
|
+
user = $state<CurrentUser | null>(null)
|
|
21
|
+
page = $state<ManagementPage>('settings')
|
|
22
|
+
locale = $state(getLocale())
|
|
23
|
+
initializing = $state(true)
|
|
24
|
+
loading = $state(false)
|
|
25
|
+
saving = $state(false)
|
|
26
|
+
error = $state('')
|
|
27
|
+
notice = $state('')
|
|
28
|
+
users = $state<AdminUser[]>([])
|
|
29
|
+
usersTotal = $state(0)
|
|
30
|
+
usersPage = $state(1)
|
|
31
|
+
usersSearch = $state('')
|
|
32
|
+
usersStatus = $state('all')
|
|
33
|
+
roles = $state<AdminRole[]>([])
|
|
34
|
+
audit = $state<AdminAuditLog[]>([])
|
|
35
|
+
auditTotal = $state(0)
|
|
36
|
+
auditPage = $state(1)
|
|
37
|
+
auditAction = $state('')
|
|
38
|
+
auditResource = $state('')
|
|
39
|
+
auditStartDate = $state('')
|
|
40
|
+
auditEndDate = $state('')
|
|
41
|
+
readonly pageSize = 20
|
|
42
|
+
private requestVersion = 0
|
|
43
|
+
private sessionEpoch = 0
|
|
44
|
+
private onLocaleChange?: (locale: string) => void
|
|
45
|
+
|
|
46
|
+
get isAdmin(): boolean {
|
|
47
|
+
return this.user?.roles.includes('admin') ?? false
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async initialize(options: ManagementApiOptions, onLocaleChange?: (locale: string) => void): Promise<void> {
|
|
51
|
+
this.onLocaleChange = onLocaleChange
|
|
52
|
+
this.initializing = true
|
|
53
|
+
this.error = ''
|
|
54
|
+
try {
|
|
55
|
+
const init = await initializeManagementApi({ ...options, onSessionExpired: () => this.expireSession() })
|
|
56
|
+
if (!init.success) {
|
|
57
|
+
this.error = init.error.code === HaiApiClientError.CONFIG_ERROR.code ? m.api_not_configured() : m.home_error()
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
const info = await managementApi.app.info()
|
|
61
|
+
if (!info.success) {
|
|
62
|
+
this.error = m.home_error()
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
this.info = info.data
|
|
66
|
+
this.applyLocale(info.data.defaultLocale)
|
|
67
|
+
// 默认 Cookie 存储通过 api-client 的加密刷新链路恢复会话。
|
|
68
|
+
const me = await managementApi.iam.auth.currentUser()
|
|
69
|
+
if (me.success) {
|
|
70
|
+
this.setUser(me.data)
|
|
71
|
+
await this.navigate(this.isAdmin ? 'users' : 'settings')
|
|
72
|
+
}
|
|
73
|
+
else if (me.error.httpStatus !== 401) {
|
|
74
|
+
this.error = m.operation_failed()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
this.error = m.home_error()
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
this.initializing = false
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async close(): Promise<void> {
|
|
86
|
+
this.requestVersion++
|
|
87
|
+
this.sessionEpoch++
|
|
88
|
+
this.loading = false
|
|
89
|
+
await managementApi.close()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async login(data: LoginFormData): Promise<void> {
|
|
93
|
+
await this.mutate(async () => {
|
|
94
|
+
const login = await managementApi.iam.auth.login({ identifier: data.username, password: data.password })
|
|
95
|
+
if (!login.success) {
|
|
96
|
+
this.error = login.error.httpStatus && login.error.httpStatus < 500 ? m.login_invalid() : m.network_error()
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
await managementApi.auth.setTokens(login.data.tokens)
|
|
100
|
+
const me = await this.request(() => managementApi.iam.auth.currentUser())
|
|
101
|
+
if (!me?.success)
|
|
102
|
+
return
|
|
103
|
+
this.setUser(me.data)
|
|
104
|
+
await this.navigate(this.isAdmin ? 'users' : 'settings')
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async logout(): Promise<void> {
|
|
109
|
+
await this.mutate(async () => {
|
|
110
|
+
const result = await this.request(() => managementApi.iam.auth.logout({}))
|
|
111
|
+
if (!result?.success)
|
|
112
|
+
return
|
|
113
|
+
await managementApi.auth.clear()
|
|
114
|
+
this.clearSession()
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async navigate(page: ManagementPage): Promise<void> {
|
|
119
|
+
if (!this.user)
|
|
120
|
+
return
|
|
121
|
+
this.requestVersion++
|
|
122
|
+
this.loading = false
|
|
123
|
+
this.page = this.isAdmin ? page : 'settings'
|
|
124
|
+
this.error = ''
|
|
125
|
+
this.notice = ''
|
|
126
|
+
if (this.page === 'users')
|
|
127
|
+
await this.loadUsers()
|
|
128
|
+
else if (this.page === 'roles')
|
|
129
|
+
await this.loadRoles()
|
|
130
|
+
else if (this.page === 'audit')
|
|
131
|
+
await this.loadAudit()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async loadUsers(page = 1): Promise<void> {
|
|
135
|
+
await this.load(async (version) => {
|
|
136
|
+
const result = await this.request(() => managementApi.admin.users.list({
|
|
137
|
+
page,
|
|
138
|
+
pageSize: this.pageSize,
|
|
139
|
+
search: this.usersSearch.trim() || undefined,
|
|
140
|
+
enabled: this.usersStatus === 'all' ? undefined : this.usersStatus === 'enabled',
|
|
141
|
+
}), version)
|
|
142
|
+
if (result?.success && version === this.requestVersion) {
|
|
143
|
+
this.users = result.data.items.map(user => ({ ...user, avatarUrl: resolveAvatarUrl(user.avatarUrl) }))
|
|
144
|
+
this.usersTotal = result.data.total
|
|
145
|
+
this.usersPage = result.data.page
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async loadRoles(): Promise<void> {
|
|
151
|
+
await this.load(async (version) => {
|
|
152
|
+
const result = await this.request(() => managementApi.admin.roles.list(), version)
|
|
153
|
+
if (result?.success && version === this.requestVersion)
|
|
154
|
+
this.roles = result.data.items
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async loadAudit(page = 1): Promise<void> {
|
|
159
|
+
if (this.auditStartDate && this.auditEndDate && this.auditStartDate > this.auditEndDate) {
|
|
160
|
+
this.error = m.audit_date_invalid()
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
await this.load(async (version) => {
|
|
164
|
+
const result = await this.request(() => managementApi.admin.audit.list({
|
|
165
|
+
page,
|
|
166
|
+
pageSize: this.pageSize,
|
|
167
|
+
action: this.auditAction.trim() || undefined,
|
|
168
|
+
resource: this.auditResource.trim() || undefined,
|
|
169
|
+
startDate: this.auditStartDate ? new SvelteDate(`${this.auditStartDate}T00:00:00`).toISOString() : undefined,
|
|
170
|
+
endDate: this.auditEndDate ? new SvelteDate(`${this.auditEndDate}T23:59:59.999`).toISOString() : undefined,
|
|
171
|
+
}), version)
|
|
172
|
+
if (result?.success && version === this.requestVersion) {
|
|
173
|
+
this.audit = result.data.items
|
|
174
|
+
this.auditTotal = result.data.total
|
|
175
|
+
this.auditPage = result.data.page
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async saveUser(data: AdminCreateUser | AdminUpdateUser): Promise<boolean> {
|
|
181
|
+
let saved = false
|
|
182
|
+
await this.mutate(async () => {
|
|
183
|
+
const result = 'id' in data
|
|
184
|
+
? await this.request(() => managementApi.admin.users.update(data))
|
|
185
|
+
: await this.request(() => managementApi.admin.users.create(data))
|
|
186
|
+
if (!result?.success)
|
|
187
|
+
return
|
|
188
|
+
saved = true
|
|
189
|
+
await this.loadUsers(this.usersPage)
|
|
190
|
+
this.notice = m.saved()
|
|
191
|
+
})
|
|
192
|
+
return saved
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async deleteUser(user: AdminUser): Promise<boolean> {
|
|
196
|
+
let deleted = false
|
|
197
|
+
await this.mutate(async () => {
|
|
198
|
+
const result = await this.request(() => managementApi.admin.users.delete({ id: user.id }))
|
|
199
|
+
if (!result?.success)
|
|
200
|
+
return
|
|
201
|
+
deleted = true
|
|
202
|
+
const page = this.users.length === 1 ? Math.max(1, this.usersPage - 1) : this.usersPage
|
|
203
|
+
await this.loadUsers(page)
|
|
204
|
+
this.notice = m.user_deleted()
|
|
205
|
+
})
|
|
206
|
+
return deleted
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async saveRole(id: string, description: string): Promise<boolean> {
|
|
210
|
+
let saved = false
|
|
211
|
+
await this.mutate(async () => {
|
|
212
|
+
const result = await this.request(() => managementApi.admin.roles.update({ id, description }))
|
|
213
|
+
if (!result?.success)
|
|
214
|
+
return
|
|
215
|
+
saved = true
|
|
216
|
+
await this.loadRoles()
|
|
217
|
+
this.notice = m.saved()
|
|
218
|
+
})
|
|
219
|
+
return saved
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async saveProfile(data: UserProfileSubmitData): Promise<void> {
|
|
223
|
+
await this.savePreferences({ displayName: data.displayName?.trim() ?? '', email: data.email?.trim() ?? '' })
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async savePreferences(data: ProfileUpdate): Promise<void> {
|
|
227
|
+
await this.mutate(async () => {
|
|
228
|
+
const result = await this.request(() => managementApi.profile.update(data))
|
|
229
|
+
if (!result?.success)
|
|
230
|
+
return
|
|
231
|
+
const me = await this.request(() => managementApi.iam.auth.currentUser())
|
|
232
|
+
if (me?.success)
|
|
233
|
+
this.setUser(me.data)
|
|
234
|
+
this.notice = m.saved()
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async changePassword(data: ChangePasswordFormData): Promise<void> {
|
|
239
|
+
await this.mutate(async () => {
|
|
240
|
+
if (!data.oldPassword || data.newPassword === data.oldPassword || data.newPassword !== data.confirmPassword || data.newPassword.length < 8) {
|
|
241
|
+
this.error = m.validation_failed()
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
const oldPassword = data.oldPassword
|
|
245
|
+
const result = await this.request(() => managementApi.iam.auth.changePassword({ oldPassword, newPassword: data.newPassword }))
|
|
246
|
+
if (!result?.success)
|
|
247
|
+
return
|
|
248
|
+
await managementApi.auth.clear()
|
|
249
|
+
this.clearSession()
|
|
250
|
+
this.notice = m.password_changed()
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async uploadAvatar(file: File, userId?: string): Promise<void> {
|
|
255
|
+
await this.mutate(async () => {
|
|
256
|
+
const payload = await this.readAvatar(file)
|
|
257
|
+
if (!payload)
|
|
258
|
+
return
|
|
259
|
+
const result = userId
|
|
260
|
+
? await this.request(() => managementApi.admin.users.avatar({ id: userId, ...payload }))
|
|
261
|
+
: await this.request(() => managementApi.profile.avatar(payload))
|
|
262
|
+
if (!result?.success)
|
|
263
|
+
return
|
|
264
|
+
if (!userId || userId === this.user?.id) {
|
|
265
|
+
const me = await this.request(() => managementApi.iam.auth.currentUser())
|
|
266
|
+
if (me?.success)
|
|
267
|
+
this.setUser(me.data)
|
|
268
|
+
}
|
|
269
|
+
if (userId)
|
|
270
|
+
await this.loadUsers(this.usersPage)
|
|
271
|
+
this.notice = m.avatar_saved()
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
formatDate(value: string): string {
|
|
276
|
+
return new Intl.DateTimeFormat(this.locale, { dateStyle: 'medium', timeStyle: 'short' }).format(new SvelteDate(value))
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private async readAvatar(file: File): Promise<AvatarUpload | undefined> {
|
|
280
|
+
if (file.size > 512 * 1024 || !['image/png', 'image/jpeg', 'image/webp'].includes(file.type)) {
|
|
281
|
+
this.error = m.avatar_invalid()
|
|
282
|
+
return undefined
|
|
283
|
+
}
|
|
284
|
+
const bytes = new Uint8Array(await file.arrayBuffer())
|
|
285
|
+
let binary = ''
|
|
286
|
+
for (const byte of bytes)
|
|
287
|
+
binary += String.fromCharCode(byte)
|
|
288
|
+
return { contentType: file.type as AvatarUpload['contentType'], base64: btoa(binary) }
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private setUser(user: CurrentUser): void {
|
|
292
|
+
if (this.user?.id !== user.id)
|
|
293
|
+
this.sessionEpoch++
|
|
294
|
+
this.user = { ...user, avatarUrl: resolveAvatarUrl(user.avatarUrl) }
|
|
295
|
+
this.applyLocale(typeof user.metadata?.locale === 'string' ? user.metadata.locale : this.info?.defaultLocale ?? getLocale())
|
|
296
|
+
if (typeof user.metadata?.theme === 'string' && AVAILABLE_THEMES.includes(user.metadata.theme))
|
|
297
|
+
themeStore.set(user.metadata.theme)
|
|
298
|
+
else
|
|
299
|
+
themeStore.set(DEFAULT_THEME)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private applyLocale(value: string): void {
|
|
303
|
+
const locale = locales.find(locale => locale === value)
|
|
304
|
+
if (!locale)
|
|
305
|
+
return
|
|
306
|
+
this.locale = locale
|
|
307
|
+
setLocale(locale, { reload: false })
|
|
308
|
+
core.i18n.setGlobalLocale(locale)
|
|
309
|
+
if (typeof document !== 'undefined')
|
|
310
|
+
document.documentElement.lang = locale
|
|
311
|
+
this.onLocaleChange?.(locale)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private clearSession(): void {
|
|
315
|
+
this.requestVersion++
|
|
316
|
+
this.sessionEpoch++
|
|
317
|
+
this.loading = false
|
|
318
|
+
this.user = null
|
|
319
|
+
this.users = []
|
|
320
|
+
this.roles = []
|
|
321
|
+
this.audit = []
|
|
322
|
+
this.page = 'settings'
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private expireSession(): void {
|
|
326
|
+
if (!this.user)
|
|
327
|
+
return
|
|
328
|
+
this.clearSession()
|
|
329
|
+
this.error = m.session_expired()
|
|
330
|
+
void managementApi.auth.clear()
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private async request<T>(task: () => Promise<HaiResult<T>>, version?: number): Promise<HaiResult<T> | undefined> {
|
|
334
|
+
const epoch = this.sessionEpoch
|
|
335
|
+
const isCurrent = () => epoch === this.sessionEpoch && (version === undefined || version === this.requestVersion)
|
|
336
|
+
try {
|
|
337
|
+
const result = await task()
|
|
338
|
+
if (!isCurrent())
|
|
339
|
+
return undefined
|
|
340
|
+
if (!result.success) {
|
|
341
|
+
if (result.error.httpStatus === 401 && this.user) {
|
|
342
|
+
this.expireSession()
|
|
343
|
+
}
|
|
344
|
+
else if (result.error.httpStatus === 403 && this.user) {
|
|
345
|
+
const current = await managementApi.iam.auth.currentUser()
|
|
346
|
+
if (!isCurrent())
|
|
347
|
+
return undefined
|
|
348
|
+
if (current.success) {
|
|
349
|
+
this.setUser(current.data)
|
|
350
|
+
if (!this.isAdmin) {
|
|
351
|
+
this.requestVersion++
|
|
352
|
+
this.loading = false
|
|
353
|
+
this.users = []
|
|
354
|
+
this.roles = []
|
|
355
|
+
this.audit = []
|
|
356
|
+
this.page = 'settings'
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
this.error = m.forbidden()
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
this.error = result.error.httpStatus === 403 ? m.forbidden() : result.error.httpStatus === 409 ? m.conflict() : result.error.httpStatus === 400 || result.error.httpStatus === 422 ? m.validation_failed() : m.operation_failed()
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return result
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
if (isCurrent())
|
|
369
|
+
this.error = m.network_error()
|
|
370
|
+
return undefined
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private async load(task: (version: number) => Promise<void>): Promise<void> {
|
|
375
|
+
const version = ++this.requestVersion
|
|
376
|
+
this.loading = true
|
|
377
|
+
this.error = ''
|
|
378
|
+
await task(version)
|
|
379
|
+
if (version === this.requestVersion)
|
|
380
|
+
this.loading = false
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private async mutate(task: () => Promise<void>): Promise<void> {
|
|
384
|
+
if (this.saving)
|
|
385
|
+
return
|
|
386
|
+
this.saving = true
|
|
387
|
+
this.error = ''
|
|
388
|
+
this.notice = ''
|
|
389
|
+
try {
|
|
390
|
+
await task()
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
this.error = m.network_error()
|
|
394
|
+
}
|
|
395
|
+
finally {
|
|
396
|
+
this.saving = false
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
@@ -1,54 +1,22 @@
|
|
|
1
|
-
{
|
|
2
|
-
const STORAGE_KEY = 'hai-theme'
|
|
3
|
-
const DEFAULT_THEME = '{{fullstack.theme.defaultTheme}}'
|
|
4
|
-
export const AVAILABLE_THEMES: readonly string[] = [{{#each fullstack.theme.themes}}'{{this}}'{{#unless @last}}, {{/unless}}{{/each}}]
|
|
5
|
-
|
|
6
|
-
function isBrowser(): boolean {
|
|
7
|
-
return typeof window !== 'undefined' && typeof document !== 'undefined'
|
|
8
|
-
}
|
|
1
|
+
import { applyTheme, getSavedTheme } from '@h-ai/ui/theme'
|
|
9
2
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return DEFAULT_THEME
|
|
13
|
-
try {
|
|
14
|
-
const stored = window.localStorage.getItem(STORAGE_KEY)
|
|
15
|
-
if (stored && AVAILABLE_THEMES.includes(stored))
|
|
16
|
-
return stored
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
// localStorage 不可用时(隐私模式 / SSR),回退到默认主题
|
|
20
|
-
}
|
|
21
|
-
return DEFAULT_THEME
|
|
22
|
-
}
|
|
3
|
+
export const DEFAULT_THEME = '{{fullstack.theme.defaultTheme}}'
|
|
4
|
+
export const AVAILABLE_THEMES: readonly string[] = [{{#each fullstack.theme.themes}}'{{this}}'{{#unless @last}}, {{/unless}}{{/each}}]
|
|
23
5
|
|
|
6
|
+
/** 只维护响应式状态和已编译主题白名单,DOM 与安全持久化复用 UI 公共工具。 */
|
|
24
7
|
class ThemeState {
|
|
25
8
|
current = $state<string>(DEFAULT_THEME)
|
|
26
9
|
|
|
27
10
|
constructor() {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
this.apply(this.current)
|
|
11
|
+
const saved = getSavedTheme(DEFAULT_THEME)
|
|
12
|
+
this.set(AVAILABLE_THEMES.includes(saved) ? saved : DEFAULT_THEME)
|
|
31
13
|
}
|
|
32
14
|
|
|
33
15
|
set(value: string): void {
|
|
34
16
|
if (!AVAILABLE_THEMES.includes(value))
|
|
35
17
|
return
|
|
36
18
|
this.current = value
|
|
37
|
-
|
|
38
|
-
if (isBrowser()) {
|
|
39
|
-
try {
|
|
40
|
-
window.localStorage.setItem(STORAGE_KEY, value)
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
// 忽略 localStorage 写入失败
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
private apply(value: string): void {
|
|
49
|
-
if (!isBrowser())
|
|
50
|
-
return
|
|
51
|
-
document.documentElement.dataset.theme = value
|
|
19
|
+
applyTheme(value)
|
|
52
20
|
}
|
|
53
21
|
}
|
|
54
22
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default {}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { EncryptedPayload } from '@h-ai/crypto'
|
|
2
|
+
import { HaiApiClientError } from '@h-ai/api-client'
|
|
3
|
+
import { crypto, TRANSPORT_PROTOCOL } from '@h-ai/crypto'
|
|
4
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
|
|
5
|
+
import { initializeManagementApi, managementApi, resolveAvatarUrl } from '../src/lib/api/api-client.js'
|
|
6
|
+
|
|
7
|
+
const runtimeInfo = {
|
|
8
|
+
name: 'Configured workspace',
|
|
9
|
+
version: '9.8.7',
|
|
10
|
+
env: 'test',
|
|
11
|
+
defaultLocale: 'zh-CN',
|
|
12
|
+
supportedLocales: ['zh-CN', 'en-US'],
|
|
13
|
+
startedAt: '2026-01-01T00:00:00.000Z',
|
|
14
|
+
frontends: ['web'],
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** 使用真实 crypto 公共入口模拟对端,覆盖握手及双向加解密。 */
|
|
18
|
+
function createServerFetch() {
|
|
19
|
+
const created = crypto.transport.createServer()
|
|
20
|
+
if (!created.success)
|
|
21
|
+
throw new Error(created.error.message)
|
|
22
|
+
const server = created.data
|
|
23
|
+
const requests: Request[] = []
|
|
24
|
+
const messages: string[] = []
|
|
25
|
+
const fetchFn: typeof fetch = async (input, init) => {
|
|
26
|
+
const request = new Request(input, init)
|
|
27
|
+
requests.push(request.clone())
|
|
28
|
+
const url = new URL(request.url)
|
|
29
|
+
if (url.pathname.endsWith('/_hai/key-exchange')) {
|
|
30
|
+
const body = await request.json() as { clientPublicKey: string }
|
|
31
|
+
const clientId = await server.registerClientKey(body.clientPublicKey)
|
|
32
|
+
return Response.json({ serverPublicKey: server.getServerPublicKey(), clientId })
|
|
33
|
+
}
|
|
34
|
+
const clientId = request.headers.get(TRANSPORT_PROTOCOL.CLIENT_ID_HEADER)
|
|
35
|
+
if (!clientId)
|
|
36
|
+
return Response.json({ success: false }, { status: 400 })
|
|
37
|
+
let data: unknown = runtimeInfo
|
|
38
|
+
if (url.pathname.endsWith('/app/echo')) {
|
|
39
|
+
expect(request.headers.get(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER)).toBe(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER_VALUE)
|
|
40
|
+
const encrypted = await request.json() as EncryptedPayload
|
|
41
|
+
expect(JSON.stringify(encrypted)).not.toContain('private message')
|
|
42
|
+
const decrypted = server.decryptRequest(encrypted)
|
|
43
|
+
expect(decrypted.success).toBe(true)
|
|
44
|
+
if (!decrypted.success)
|
|
45
|
+
throw new Error(decrypted.error.message)
|
|
46
|
+
const payload = JSON.parse(decrypted.data) as { message: string }
|
|
47
|
+
messages.push(payload.message)
|
|
48
|
+
data = { message: payload.message, echoedAt: '2026-01-01T00:00:00.000Z', requestId: 'test-request' }
|
|
49
|
+
}
|
|
50
|
+
const response = await server.encryptResponse(clientId, JSON.stringify({ success: true, data }))
|
|
51
|
+
if (!response.success)
|
|
52
|
+
throw new Error(response.error.message)
|
|
53
|
+
return Response.json(response.data, { headers: { [TRANSPORT_PROTOCOL.ENCRYPTED_HEADER]: TRANSPORT_PROTOCOL.ENCRYPTED_HEADER_VALUE } })
|
|
54
|
+
}
|
|
55
|
+
return { fetchFn, requests, messages }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('shared API transport', () => {
|
|
59
|
+
beforeAll(async () => {
|
|
60
|
+
expect((await crypto.init()).success).toBe(true)
|
|
61
|
+
})
|
|
62
|
+
afterAll(async () => {
|
|
63
|
+
await managementApi.close()
|
|
64
|
+
await crypto.close()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('decrypts configured core metadata and includes refresh cookies', async () => {
|
|
68
|
+
const server = createServerFetch()
|
|
69
|
+
expect((await initializeManagementApi({ fetchFn: server.fetchFn, baseUrl: 'https://api.example.com', clientName: 'configured-client' })).success).toBe(true)
|
|
70
|
+
const info = await managementApi.app.info()
|
|
71
|
+
expect(info.success).toBe(true)
|
|
72
|
+
if (info.success)
|
|
73
|
+
expect(info.data).toEqual(runtimeInfo)
|
|
74
|
+
expect(server.requests.every(request => request.credentials === 'include')).toBe(true)
|
|
75
|
+
expect(server.requests.at(-1)?.headers.get('x-request-client')).toBe('configured-client')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('uses the API origin injected at container startup', async () => {
|
|
79
|
+
Object.assign(globalThis, { __HAI_PUBLIC_CONFIG__: { apiBase: 'https://runtime.example.com' } })
|
|
80
|
+
const server = createServerFetch()
|
|
81
|
+
try {
|
|
82
|
+
expect((await initializeManagementApi({ fetchFn: server.fetchFn })).success).toBe(true)
|
|
83
|
+
expect((await managementApi.app.info()).success).toBe(true)
|
|
84
|
+
expect(server.requests.every(request => request.url.startsWith('https://runtime.example.com/api/v1/'))).toBe(true)
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
Reflect.deleteProperty(globalThis, '__HAI_PUBLIC_CONFIG__')
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('encrypts business input and preserves the native token transport header', async () => {
|
|
92
|
+
const server = createServerFetch()
|
|
93
|
+
const init = await initializeManagementApi({ fetchFn: server.fetchFn, baseUrl: 'https://api.example.com/api/v1', headers: { 'X-HAI-Token-Transport': 'body' } })
|
|
94
|
+
expect(init.success).toBe(true)
|
|
95
|
+
try {
|
|
96
|
+
const result = await managementApi.app.echo({ message: 'private message' })
|
|
97
|
+
expect(result.success).toBe(true)
|
|
98
|
+
if (result.success)
|
|
99
|
+
expect(result.data.message).toBe('private message')
|
|
100
|
+
expect(server.messages).toEqual(['private message'])
|
|
101
|
+
expect(server.requests.at(-1)?.headers.get('X-HAI-Token-Transport')).toBe('body')
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
await managementApi.close()
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('resolves avatars only under the configured API origin', async () => {
|
|
109
|
+
const server = createServerFetch()
|
|
110
|
+
expect((await initializeManagementApi({ fetchFn: server.fetchFn, baseUrl: 'https://trusted.example.com/api/v1' })).success).toBe(true)
|
|
111
|
+
try {
|
|
112
|
+
const path = '/api/v1/assets/avatars/user-id/asset-id.png'
|
|
113
|
+
expect(resolveAvatarUrl(path)).toBe(`https://trusted.example.com${path}`)
|
|
114
|
+
expect(resolveAvatarUrl(`https://host-injection.example.com${path}`)).toBe(`https://trusted.example.com${path}`)
|
|
115
|
+
expect(resolveAvatarUrl('https://host-injection.example.com/pixel.png')).toBeUndefined()
|
|
116
|
+
expect(resolveAvatarUrl('/api/v1/assets/avatars/../secrets.txt')).toBeUndefined()
|
|
117
|
+
expect(resolveAvatarUrl('data:image/svg+xml,<svg/>')).toBeUndefined()
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
await managementApi.close()
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('requires an explicit API origin for a native build', async () => {
|
|
125
|
+
vi.stubEnv('PUBLIC_API_BASE', '')
|
|
126
|
+
try {
|
|
127
|
+
const result = await initializeManagementApi({ requireApiBase: true })
|
|
128
|
+
expect(result.success).toBe(false)
|
|
129
|
+
if (!result.success)
|
|
130
|
+
expect(result.error.code).toBe(HaiApiClientError.CONFIG_ERROR.code)
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
vi.unstubAllEnvs()
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('preserves a configured API prefix for transport and avatars', async () => {
|
|
138
|
+
const server = createServerFetch()
|
|
139
|
+
expect((await initializeManagementApi({ fetchFn: server.fetchFn, baseUrl: 'https://trusted.example.com/custom-api/' })).success).toBe(true)
|
|
140
|
+
try {
|
|
141
|
+
expect((await managementApi.app.info()).success).toBe(true)
|
|
142
|
+
expect(server.requests.every(request => request.url.startsWith('https://trusted.example.com/custom-api/'))).toBe(true)
|
|
143
|
+
const avatar = '/custom-api/assets/avatars/user-id/asset-id.png'
|
|
144
|
+
expect(resolveAvatarUrl(avatar)).toBe(`https://trusted.example.com${avatar}`)
|
|
145
|
+
expect(resolveAvatarUrl('/api/v1/assets/avatars/user-id/asset-id.png')).toBeUndefined()
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
await managementApi.close()
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
})
|