@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
|
@@ -113,11 +113,18 @@ describe('{{fullstack.servPackageName}} mini administration', () => {
|
|
|
113
113
|
}
|
|
114
114
|
})
|
|
115
115
|
|
|
116
|
-
it('
|
|
116
|
+
it('mounts public auth routes while keeping default IAM administration routes private', async () => {
|
|
117
|
+
const config = await transport.encryptedFetch(`http://localhost${PREFIX}/auth/config`)
|
|
118
|
+
expect(config.status).toBe(200)
|
|
119
|
+
for (const route of ['/auth/register', '/auth/login/ldap', '/auth/login/otp', '/auth/otp/send', '/auth/password-reset/request', '/auth/password-reset/confirm']) {
|
|
120
|
+
const response = await transport.encryptedFetch(`http://localhost${PREFIX}${route}`, {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
headers: { 'content-type': 'application/json' },
|
|
123
|
+
body: '{}',
|
|
124
|
+
})
|
|
125
|
+
expect(response.status).toBe(400)
|
|
126
|
+
}
|
|
117
127
|
for (const [route, method] of [
|
|
118
|
-
['/auth/register', 'POST'],
|
|
119
|
-
['/auth/login/otp', 'POST'],
|
|
120
|
-
['/auth/otp/send', 'POST'],
|
|
121
128
|
['/iam/users', 'GET'],
|
|
122
129
|
['/iam/roles', 'POST'],
|
|
123
130
|
] as const) {
|
|
@@ -25,8 +25,20 @@ Language and theme preferences are saved in IAM user metadata, restored after lo
|
|
|
25
25
|
|
|
26
26
|
Shared page copy belongs in `messages/{zh-CN,en-US}.json`. Frontend-specific copy belongs in each app's own messages directory. Keep `baseLocale` and `locales` aligned across all enabled projects, then run root `pnpm i18n:compile`; do not edit generated Paraglide files.
|
|
27
27
|
|
|
28
|
+
All frontend and shared projects load the installed message-format plugin from the workspace root `node_modules`, matching the scaffold's hoisted dependency layout. Compilation uses the workspace dependency version and does not fetch a plugin from a CDN.
|
|
29
|
+
|
|
28
30
|
The scaffold theme configuration determines the themes compiled into frontend CSS. Personal settings offer only those available themes. Customize the common layout in `src/lib/components/AppShell.svelte`, the management pages in adjacent components, and business behavior in `src/lib/stores/management-store.svelte.ts`.
|
|
29
31
|
|
|
30
32
|
## Validation
|
|
31
33
|
|
|
32
34
|
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.
|
|
35
|
+
|
|
36
|
+
## IAM configuration
|
|
37
|
+
|
|
38
|
+
The service's `config/_iam.yml` controls login methods, registration, password reset, agreements and `ui` branding. Email and SMS use providers/templates in `_reach.yml` or custom callbacks. `AuthPanel` uses the public configuration and framework auth actions to manage tokens, resend countdowns and errors. All service nodes must share the same database and Redis; memory cache is for a single process.
|
|
39
|
+
|
|
40
|
+
LDAP and Reach are optional. Install ldapts in the IAM service workspace only when using its default LDAP client, then enable login.ldap and set connection settings. Custom onOtpSendEmail/onOtpSendSms/onPasswordResetRequest callbacks work without Reach and must reject on delivery failure. Agreements open in a Modal. LoginForm/RegisterForm support header/extraFields/footer snippets; AuthPanel forwards loginHeader/loginFields/loginFooter and registerHeader/registerFields/registerFooter. See the generated hai-iam/hai-ui skills for configuration and examples.
|
|
41
|
+
|
|
42
|
+
协议支持配置中的 userAgreementMarkdown/privacyPolicyMarkdown 正文及 URL;同主题优先 Markdown,由安全渲染器在弹窗显示。
|
|
43
|
+
|
|
44
|
+
See `.agents/skills/hai-iam/reference.md` in the generated application for the complete contract.
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"preferences_title": "Preferences",
|
|
40
40
|
"preferences_description": "Your language and appearance preferences follow your account across devices.",
|
|
41
41
|
"password_title": "Change password",
|
|
42
|
-
"password_rules": "
|
|
42
|
+
"password_rules": "Choose a different password.",
|
|
43
43
|
"password_changed": "Password updated. Please sign in with your new password.",
|
|
44
44
|
"avatar_hint": "Upload a PNG, JPEG or WebP image, up to 512 KB.",
|
|
45
45
|
"avatar_invalid": "Choose a PNG, JPEG or WebP image no larger than 512 KB.",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"user_role": "Role",
|
|
56
56
|
"user_status": "Status",
|
|
57
57
|
"user_create": "Add user",
|
|
58
|
-
"initial_password_rules": "Use at least
|
|
58
|
+
"initial_password_rules": "Use at least {min} characters.",
|
|
59
59
|
"account_enabled": "Allow sign-in",
|
|
60
60
|
"users_empty": "No matching accounts. Try another search or create a user.",
|
|
61
61
|
"user_edit": "Edit user",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"preferences_title": "偏好设置",
|
|
40
40
|
"preferences_description": "语言与界面风格保存到个人账户,在不同设备上保持一致。",
|
|
41
41
|
"password_title": "修改密码",
|
|
42
|
-
"password_rules": "
|
|
42
|
+
"password_rules": "请使用与旧密码不同的新密码。",
|
|
43
43
|
"password_changed": "密码已修改,请使用新密码重新登录。",
|
|
44
44
|
"avatar_hint": "支持 PNG、JPEG 和 WebP 图片,大小不超过 512 KB。",
|
|
45
45
|
"avatar_invalid": "请选择不超过 512 KB 的 PNG、JPEG 或 WebP 图片。",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"user_role": "角色",
|
|
56
56
|
"user_status": "状态",
|
|
57
57
|
"user_create": "新增用户",
|
|
58
|
-
"initial_password_rules": "密码至少
|
|
58
|
+
"initial_password_rules": "密码至少 {min} 个字符。",
|
|
59
59
|
"account_enabled": "启用账户",
|
|
60
60
|
"users_empty": "没有匹配的账户,请调整查询条件或新增用户。",
|
|
61
61
|
"user_edit": "编辑用户",
|
package/templates/apps/fullstack/packages/{{projectName}}-shared/project.inlang/settings.json.hbs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"baseLocale": "{{fullstack.i18n.baseLocale}}",
|
|
4
4
|
"locales": [{{#each fullstack.i18n.locales}}"{{this}}"{{#unless @last}}, {{/unless}}{{/each}}],
|
|
5
5
|
"modules": [
|
|
6
|
-
"
|
|
6
|
+
"../../node_modules/@inlang/plugin-message-format/dist/index.js"
|
|
7
7
|
],
|
|
8
8
|
"plugin.inlang.messageFormat": {
|
|
9
9
|
"pathPattern": "./messages/{locale}.json"
|
package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/api-client.ts.hbs
CHANGED
|
@@ -86,3 +86,6 @@ function normalizeApiBaseUrl(value: string): string {
|
|
|
86
86
|
url.pathname = '/api/v1'
|
|
87
87
|
return url.toString().replace(/\/+$/, '')
|
|
88
88
|
}
|
|
89
|
+
|
|
90
|
+
/** 标准 IAM 表单动作,平台安全 TokenStorage 由原有初始化配置决定。 */
|
|
91
|
+
export const managementAuthActions = apiClient.createAuthActions(managementApi)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
<script lang=
|
|
1
|
+
<script lang='ts'>
|
|
2
2
|
import type { ManagementApiOptions } from '../api/api-client.js'
|
|
3
3
|
import { Alert, Skeleton } from '@h-ai/ui/components/compounds/index'
|
|
4
4
|
import { Button } from '@h-ai/ui/components/primitives/index'
|
|
5
|
-
import {
|
|
5
|
+
import { AuthPanel } from '@h-ai/ui/components/scenes/iam/index'
|
|
6
6
|
import { onMount } from 'svelte'
|
|
7
|
+
import { managementAuthActions } from '../api/api-client.js'
|
|
7
8
|
import * as m from '../paraglide/messages.js'
|
|
8
9
|
import { ManagementStore } from '../stores/management-store.svelte.js'
|
|
9
10
|
import AppShell from './AppShell.svelte'
|
|
@@ -15,7 +16,16 @@
|
|
|
15
16
|
}
|
|
16
17
|
const { platform, apiOptions = {}, onLocaleChange }: Props = $props()
|
|
17
18
|
const store = new ManagementStore()
|
|
18
|
-
|
|
19
|
+
let resetToken = $state('')
|
|
20
|
+
async function authenticated() {
|
|
21
|
+
await store.authenticated()
|
|
22
|
+
if (resetToken) {
|
|
23
|
+
const url = new URL(location.href)
|
|
24
|
+
url.searchParams.delete('token')
|
|
25
|
+
history.replaceState(history.state, '', url)
|
|
26
|
+
}
|
|
27
|
+
resetToken = ''
|
|
28
|
+
}
|
|
19
29
|
const pageComponent = $derived.by(() => {
|
|
20
30
|
if (store.isAdmin && store.page === 'users')
|
|
21
31
|
return import('./UserManagement.svelte')
|
|
@@ -26,6 +36,7 @@
|
|
|
26
36
|
return import('./ProfileSettings.svelte')
|
|
27
37
|
})
|
|
28
38
|
onMount(() => {
|
|
39
|
+
resetToken = new URL(location.href).searchParams.get('token') ?? ''
|
|
29
40
|
void store.initialize(apiOptions, onLocaleChange)
|
|
30
41
|
return () => {
|
|
31
42
|
void store.close()
|
|
@@ -39,31 +50,29 @@
|
|
|
39
50
|
|
|
40
51
|
{#key store.locale}
|
|
41
52
|
{#if store.initializing}
|
|
42
|
-
<div class=
|
|
43
|
-
{:else if !store.user}
|
|
44
|
-
<main
|
|
45
|
-
|
|
46
|
-
<
|
|
47
|
-
<
|
|
48
|
-
<
|
|
53
|
+
<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>
|
|
54
|
+
{:else if !store.user || resetToken}
|
|
55
|
+
<main data-testid='login-page'>
|
|
56
|
+
{#if !store.info || !store.authConfig}
|
|
57
|
+
<section class='mx-auto mt-20 max-w-md space-y-4 p-6'>
|
|
58
|
+
<Alert variant='error'>{store.error || m.home_error()}</Alert>
|
|
59
|
+
<Button variant='primary' onclick={() => void store.initialize(apiOptions, onLocaleChange)}>{m.home_refresh()}</Button>
|
|
60
|
+
</section>
|
|
61
|
+
{:else}
|
|
62
|
+
<div data-testid='login-form'>
|
|
63
|
+
{#if store.authConfig && store.error}<Alert variant='error' class='mx-auto max-w-lg'>{store.error}</Alert>{/if}
|
|
64
|
+
<AuthPanel config={store.authConfig} actions={managementAuthActions} {resetToken} initialMode={resetToken ? 'reset' : 'login'} error={store.authConfig ? '' : store.error} onretry={() => void store.initialize(apiOptions, onLocaleChange)} onauthenticated={authenticated} />
|
|
49
65
|
</div>
|
|
50
|
-
{
|
|
51
|
-
|
|
52
|
-
<Alert variant="error">{store.error || m.home_error()}</Alert>
|
|
53
|
-
<Button class="mt-5 w-full" variant="primary" onclick={() => void store.initialize(apiOptions, onLocaleChange)}>{m.home_refresh()}</Button>
|
|
54
|
-
{:else}
|
|
55
|
-
<LoginForm loading={store.saving} showRememberMe={false} showForgotPassword={false} errors={loginErrors} onsubmit={data => store.login(data)} data-testid="login-form" />
|
|
56
|
-
<p class="mt-6 text-center text-xs text-base-content/40"><span data-testid="app-version">v{store.info.version}</span> · <span data-testid="app-environment">{store.info.env}</span></p>
|
|
57
|
-
{/if}
|
|
58
|
-
</section>
|
|
66
|
+
<p class='fixed bottom-3 left-0 right-0 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></p>
|
|
67
|
+
{/if}
|
|
59
68
|
</main>
|
|
60
69
|
{:else}
|
|
61
70
|
<AppShell {platform} {store}>
|
|
62
|
-
<div class=
|
|
63
|
-
{#if store.error}<Alert variant=
|
|
64
|
-
{#if store.notice}<Alert variant=
|
|
71
|
+
<div class='space-y-5'>
|
|
72
|
+
{#if store.error}<Alert variant='error' data-testid='error'>{store.error}</Alert>{/if}
|
|
73
|
+
{#if store.notice}<Alert variant='success' data-testid='notice'>{store.notice}</Alert>{/if}
|
|
65
74
|
{#await pageComponent}
|
|
66
|
-
<div class=
|
|
75
|
+
<div class='space-y-4'><Skeleton variant='title' /><Skeleton count={3} /></div>
|
|
67
76
|
{:then { default: Page }}
|
|
68
77
|
<Page {store} />
|
|
69
78
|
{/await}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script lang=
|
|
1
|
+
<script lang='ts'>
|
|
2
2
|
import type { ManagementStore } from '../stores/management-store.svelte.js'
|
|
3
3
|
import { Card } from '@h-ai/ui/components/compounds/index'
|
|
4
4
|
import { Avatar, Select } from '@h-ai/ui/components/primitives/index'
|
|
@@ -14,44 +14,44 @@
|
|
|
14
14
|
const themeOptions = AVAILABLE_THEMES.map(theme => ({ value: theme, label: getThemeInfo(theme)?.name ?? theme }))
|
|
15
15
|
</script>
|
|
16
16
|
|
|
17
|
-
<section class=
|
|
17
|
+
<section class='space-y-5' data-testid='settings-page'>
|
|
18
18
|
<div>
|
|
19
|
-
<h1 class=
|
|
20
|
-
<p class=
|
|
19
|
+
<h1 class='text-xl font-semibold tracking-tight'>{m.settings_title()}</h1>
|
|
20
|
+
<p class='mt-1 text-sm text-base-content/50'>{m.settings_description()}</p>
|
|
21
21
|
</div>
|
|
22
22
|
{#if store.user}
|
|
23
|
-
<div class=
|
|
24
|
-
<Card bordered shadow={false} class=
|
|
25
|
-
<div class=
|
|
26
|
-
<Avatar name={store.user.displayName || store.user.username} src={store.user.avatarUrl} size=
|
|
27
|
-
<div class=
|
|
28
|
-
<h2 class=
|
|
29
|
-
<p class=
|
|
30
|
-
<p class=
|
|
23
|
+
<div class='grid gap-5 xl:grid-cols-[0.75fr_1.25fr]'>
|
|
24
|
+
<Card bordered shadow={false} class='h-fit'>
|
|
25
|
+
<div class='flex items-center gap-4'>
|
|
26
|
+
<Avatar name={store.user.displayName || store.user.username} src={store.user.avatarUrl} size='lg' />
|
|
27
|
+
<div class='min-w-0'>
|
|
28
|
+
<h2 class='truncate text-lg font-semibold'>{store.user.displayName || store.user.username}</h2>
|
|
29
|
+
<p class='text-sm text-base-content/50'>@{store.user.username}</p>
|
|
30
|
+
<p class='mt-2 text-xs text-primary'>{store.isAdmin ? m.role_admin() : m.role_user()}</p>
|
|
31
31
|
</div>
|
|
32
32
|
</div>
|
|
33
|
-
<p class=
|
|
33
|
+
<p class='mt-4 text-xs leading-6 text-base-content/50'>{m.avatar_hint()}</p>
|
|
34
34
|
</Card>
|
|
35
35
|
<Card title={m.profile_title()} bordered shadow={false}>
|
|
36
|
-
<UserProfile user={store.user} editable alwaysEditable fields={['avatar', 'displayName', 'email']} avatarAccept=
|
|
36
|
+
<UserProfile user={store.user} editable alwaysEditable fields={['avatar', 'displayName', 'email']} avatarAccept='image/png,image/jpeg,image/webp' avatarHint={m.avatar_hint()} loading={store.saving} onsubmit={data => store.saveProfile(data)} onavatarchange={file => void store.uploadAvatar(file)} data-testid='profile-form' />
|
|
37
37
|
</Card>
|
|
38
38
|
</div>
|
|
39
39
|
<Card title={m.preferences_title()} bordered shadow={false}>
|
|
40
|
-
<p class=
|
|
41
|
-
<div class=
|
|
42
|
-
<div class=
|
|
43
|
-
<label class=
|
|
44
|
-
<Select id=
|
|
40
|
+
<p class='mb-5 text-sm text-base-content/50'>{m.preferences_description()}</p>
|
|
41
|
+
<div class='grid gap-6 sm:grid-cols-2'>
|
|
42
|
+
<div class='space-y-2'>
|
|
43
|
+
<label class='text-sm font-medium' for='profile-language'>{m.settings_language()}</label>
|
|
44
|
+
<Select id='profile-language' value={store.locale} options={languageOptions} disabled={store.saving} onchange={locale => void store.savePreferences({ locale })} data-testid='profile-language' />
|
|
45
45
|
</div>
|
|
46
|
-
<div class=
|
|
47
|
-
<label class=
|
|
48
|
-
<Select id=
|
|
46
|
+
<div class='space-y-2'>
|
|
47
|
+
<label class='text-sm font-medium' for='profile-theme'>{m.settings_theme()}</label>
|
|
48
|
+
<Select id='profile-theme' value={themeStore.current} options={themeOptions} disabled={store.saving} onchange={theme => void store.savePreferences({ theme })} data-testid='profile-theme' />
|
|
49
49
|
</div>
|
|
50
50
|
</div>
|
|
51
51
|
</Card>
|
|
52
52
|
<Card title={m.password_title()} bordered shadow={false}>
|
|
53
|
-
<p class=
|
|
54
|
-
<div class=
|
|
53
|
+
<p class='mb-5 text-sm text-base-content/50'>{m.password_rules()}</p>
|
|
54
|
+
<div class='max-w-xl'><ChangePasswordForm loading={store.saving} passwordPolicy={store.authConfig?.password} minPasswordLength={store.authConfig?.password.minLength ?? 8} onsubmit={data => store.changePassword(data)} data-testid='password-form' /></div>
|
|
55
55
|
</Card>
|
|
56
56
|
{/if}
|
|
57
57
|
</section>
|
|
@@ -141,7 +141,7 @@
|
|
|
141
141
|
<FormField label={m.user_display_name()}><Input bind:value={displayName} maxlength={100} data-testid="user-display-name" /></FormField>
|
|
142
142
|
<FormField label={m.user_email()}><Input type="email" bind:value={email} data-testid="user-email" /></FormField>
|
|
143
143
|
{#if !editing}
|
|
144
|
-
<FormField label={m.user_password()} hint={m.initial_password_rules()} required><PasswordInput bind:value={password} autocomplete="new-password" minLength={8} data-testid="user-password" /></FormField>
|
|
144
|
+
<FormField label={m.user_password()} hint={m.initial_password_rules({ min: store.authConfig?.password.minLength ?? 8 })} required><PasswordInput bind:value={password} autocomplete="new-password" minLength={store.authConfig?.password.minLength ?? 8} data-testid="user-password" /></FormField>
|
|
145
145
|
{/if}
|
|
146
146
|
<FormField label={m.user_role()}><Select bind:value={role} options={roleOptions} disabled={editing?.id === store.user?.id} data-testid="user-role" /></FormField>
|
|
147
147
|
<Checkbox bind:checked={enabled} label={m.account_enabled()} disabled={editing?.id === store.user?.id} />
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { IamPublicConfig } from '@h-ai/api-contract'
|
|
1
2
|
import type { HaiResult } from '@h-ai/core'
|
|
2
|
-
import type { ChangePasswordFormData,
|
|
3
|
+
import type { ChangePasswordFormData, UserProfileSubmitData } from '@h-ai/ui'
|
|
3
4
|
import type { AdminAuditLog, AdminCreateUser, AdminRole, AdminUpdateUser, AdminUser, AvatarUpload, ProfileUpdate } from '{{fullstack.contractPackageName}}'
|
|
4
5
|
import type { AppInfo, ManagementApiOptions } from '../api/api-client.js'
|
|
5
6
|
import { HaiApiClientError } from '@h-ai/api-client'
|
|
@@ -17,6 +18,7 @@ export type ManagementPage = 'users' | 'roles' | 'audit' | 'settings'
|
|
|
17
18
|
/** 页面状态和业务操作在共享 store 中收敛,宿主端仅装配平台能力。 */
|
|
18
19
|
export class ManagementStore {
|
|
19
20
|
info = $state<AppInfo | null>(null)
|
|
21
|
+
authConfig = $state<IamPublicConfig | null>(null)
|
|
20
22
|
user = $state<CurrentUser | null>(null)
|
|
21
23
|
page = $state<ManagementPage>('settings')
|
|
22
24
|
locale = $state(getLocale())
|
|
@@ -63,6 +65,12 @@ export class ManagementStore {
|
|
|
63
65
|
return
|
|
64
66
|
}
|
|
65
67
|
this.info = info.data
|
|
68
|
+
const authConfig = await managementApi.iam.auth.publicConfig()
|
|
69
|
+
if (!authConfig.success) {
|
|
70
|
+
this.error = authConfig.error.message
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
this.authConfig = authConfig.data
|
|
66
74
|
this.applyLocale(info.data.defaultLocale)
|
|
67
75
|
// 默认 Cookie 存储通过 api-client 的加密刷新链路恢复会话。
|
|
68
76
|
const me = await managementApi.iam.auth.currentUser()
|
|
@@ -89,20 +97,12 @@ export class ManagementStore {
|
|
|
89
97
|
await managementApi.close()
|
|
90
98
|
}
|
|
91
99
|
|
|
92
|
-
async
|
|
93
|
-
await this.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
})
|
|
100
|
+
async authenticated(): Promise<void> {
|
|
101
|
+
const me = await this.request(() => managementApi.iam.auth.currentUser())
|
|
102
|
+
if (!me?.success)
|
|
103
|
+
return
|
|
104
|
+
this.setUser(me.data)
|
|
105
|
+
await this.navigate(this.isAdmin ? 'users' : 'settings')
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
async logout(): Promise<void> {
|
|
@@ -135,8 +135,8 @@ export class ManagementStore {
|
|
|
135
135
|
await this.load(async (version) => {
|
|
136
136
|
const result = await this.request(() => managementApi.admin.users.list({
|
|
137
137
|
page,
|
|
138
|
-
pageSize: this.pageSize,
|
|
139
|
-
search: this.usersSearch.trim() || undefined,
|
|
138
|
+
pageSize: this.pageSize,
|
|
139
|
+
search: this.usersSearch.trim() || undefined,
|
|
140
140
|
enabled: this.usersStatus === 'all' ? undefined : this.usersStatus === 'enabled',
|
|
141
141
|
}), version)
|
|
142
142
|
if (result?.success && version === this.requestVersion) {
|
|
@@ -163,8 +163,8 @@ search: this.usersSearch.trim() || undefined,
|
|
|
163
163
|
await this.load(async (version) => {
|
|
164
164
|
const result = await this.request(() => managementApi.admin.audit.list({
|
|
165
165
|
page,
|
|
166
|
-
pageSize: this.pageSize,
|
|
167
|
-
action: this.auditAction.trim() || undefined,
|
|
166
|
+
pageSize: this.pageSize,
|
|
167
|
+
action: this.auditAction.trim() || undefined,
|
|
168
168
|
resource: this.auditResource.trim() || undefined,
|
|
169
169
|
startDate: this.auditStartDate ? new SvelteDate(`${this.auditStartDate}T00:00:00`).toISOString() : undefined,
|
|
170
170
|
endDate: this.auditEndDate ? new SvelteDate(`${this.auditEndDate}T23:59:59.999`).toISOString() : undefined,
|
|
@@ -340,7 +340,7 @@ action: this.auditAction.trim() || undefined,
|
|
|
340
340
|
if (!result.success) {
|
|
341
341
|
if (result.error.httpStatus === 401 && this.user) {
|
|
342
342
|
this.expireSession()
|
|
343
|
-
}
|
|
343
|
+
}
|
|
344
344
|
else if (result.error.httpStatus === 403 && this.user) {
|
|
345
345
|
const current = await managementApi.iam.auth.currentUser()
|
|
346
346
|
if (!isCurrent())
|
package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/management-store.test.ts.hbs
CHANGED
|
@@ -3,8 +3,8 @@ import * as m from '../src/lib/paraglide/messages.js'
|
|
|
3
3
|
import { ManagementStore } from '../src/lib/stores/management-store.svelte.js'
|
|
4
4
|
|
|
5
5
|
const api = vi.hoisted(() => ({
|
|
6
|
-
auth: { clear: vi.fn()
|
|
7
|
-
iam: { auth: {
|
|
6
|
+
auth: { clear: vi.fn() },
|
|
7
|
+
iam: { auth: { logout: vi.fn(), currentUser: vi.fn() } },
|
|
8
8
|
admin: { users: { list: vi.fn() } },
|
|
9
9
|
}))
|
|
10
10
|
vi.mock('../src/lib/api/api-client.js', () => ({
|
|
@@ -35,7 +35,6 @@ function deferred<T>() {
|
|
|
35
35
|
|
|
36
36
|
beforeEach(() => {
|
|
37
37
|
vi.resetAllMocks()
|
|
38
|
-
api.iam.auth.login.mockResolvedValue({ success: true, data: { tokens: { accessToken: 'test-access', expiresIn: 3600, tokenType: 'Bearer' } } })
|
|
39
38
|
api.iam.auth.logout.mockResolvedValue({ success: true })
|
|
40
39
|
api.iam.auth.currentUser.mockResolvedValue({ success: true, data: member })
|
|
41
40
|
})
|
|
@@ -48,7 +47,7 @@ describe('management session and request ownership', () => {
|
|
|
48
47
|
store.user = administrator
|
|
49
48
|
const loading = store.loadUsers()
|
|
50
49
|
await store.logout()
|
|
51
|
-
await store.
|
|
50
|
+
await store.authenticated()
|
|
52
51
|
oldRequest.resolve(forbidden)
|
|
53
52
|
await loading
|
|
54
53
|
expect(store.user?.id).toBe(member.id)
|
|
@@ -89,13 +88,13 @@ describe('management session and request ownership', () => {
|
|
|
89
88
|
expect(api.iam.auth.currentUser).toHaveBeenCalledTimes(1)
|
|
90
89
|
})
|
|
91
90
|
|
|
92
|
-
it('reports a connection failure
|
|
93
|
-
api.iam.auth.
|
|
91
|
+
it('reports a connection failure when loading the authenticated user', async () => {
|
|
92
|
+
api.iam.auth.currentUser.mockRejectedValueOnce(new Error('Connection unavailable'))
|
|
94
93
|
const store = new ManagementStore()
|
|
95
|
-
await store.
|
|
94
|
+
await store.authenticated()
|
|
96
95
|
expect(store.error).toBe(m.network_error())
|
|
97
96
|
expect(store.error).not.toBe(m.login_invalid())
|
|
98
97
|
expect(store.saving).toBe(false)
|
|
99
|
-
expect(api.iam.auth.currentUser).
|
|
98
|
+
expect(api.iam.auth.currentUser).toHaveBeenCalledTimes(1)
|
|
100
99
|
})
|
|
101
100
|
})
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
<script lang='ts'>
|
|
2
|
+
{{#if features.iam}}
|
|
3
|
+
import type { IamPublicConfig } from '@h-ai/api-contract'
|
|
4
|
+
import { apiClient } from '@h-ai/api-client'
|
|
5
|
+
{{/if}}
|
|
2
6
|
import { applyTheme, getSavedTheme, setGlobalLocale } from '@h-ai/ui'
|
|
7
|
+
{{#if features.iam}}
|
|
8
|
+
import { AuthPanel } from '@h-ai/ui/components/scenes/iam/index'
|
|
9
|
+
{{/if}}
|
|
3
10
|
import { onMount } from 'svelte'
|
|
4
11
|
import { closeApi, initApi, resolveApiBase, usesNativeTokenStorage } from './lib/api.js'
|
|
5
12
|
import { appConfig, initClientCore } from './lib/app-config.js'
|
|
@@ -7,6 +14,23 @@
|
|
|
7
14
|
import * as m from './lib/paraglide/messages.js'
|
|
8
15
|
import { getLocale } from './lib/paraglide/runtime.js'
|
|
9
16
|
|
|
17
|
+
{{#if features.iam}}
|
|
18
|
+
const authActions = apiClient.createAuthActions(apiClient)
|
|
19
|
+
let authConfig = $state<IamPublicConfig | null>(null)
|
|
20
|
+
let authenticated = $state(false)
|
|
21
|
+
let resetToken = $state('')
|
|
22
|
+
|
|
23
|
+
function handleAuthenticated() {
|
|
24
|
+
if (resetToken) {
|
|
25
|
+
const url = new URL(location.href)
|
|
26
|
+
url.searchParams.delete('token')
|
|
27
|
+
history.replaceState(history.state, '', url)
|
|
28
|
+
}
|
|
29
|
+
resetToken = ''
|
|
30
|
+
authenticated = true
|
|
31
|
+
}
|
|
32
|
+
{{/if}}
|
|
33
|
+
|
|
10
34
|
type TabId = 'home' | 'tasks' | 'profile'
|
|
11
35
|
|
|
12
36
|
let booting = $state(true)
|
|
@@ -29,6 +53,16 @@
|
|
|
29
53
|
setGlobalLocale(getLocale())
|
|
30
54
|
await initCapacitor()
|
|
31
55
|
await initApi()
|
|
56
|
+
{{#if features.iam}}
|
|
57
|
+
const config = await authActions.publicConfig()
|
|
58
|
+
if (!config.success)
|
|
59
|
+
throw new Error(config.error.message)
|
|
60
|
+
authConfig = config.data
|
|
61
|
+
if (!authConfig)
|
|
62
|
+
throw new Error(m.boot_failed_title())
|
|
63
|
+
const me = await apiClient.iam.auth.currentUser()
|
|
64
|
+
authenticated = !resetToken && me.success
|
|
65
|
+
{{/if}}
|
|
32
66
|
nativeRuntime = await isNativeApp()
|
|
33
67
|
nativeTokenStorage = await usesNativeTokenStorage()
|
|
34
68
|
}
|
|
@@ -46,6 +80,9 @@
|
|
|
46
80
|
}
|
|
47
81
|
|
|
48
82
|
onMount(() => {
|
|
83
|
+
{{#if features.iam}}
|
|
84
|
+
resetToken = new URL(location.href).searchParams.get('token') ?? ''
|
|
85
|
+
{{/if}}
|
|
49
86
|
void bootstrap()
|
|
50
87
|
|
|
51
88
|
return () => {
|
|
@@ -73,6 +110,12 @@
|
|
|
73
110
|
</div>
|
|
74
111
|
</div>
|
|
75
112
|
</div>
|
|
113
|
+
{{#if features.iam}}
|
|
114
|
+
{:else if !authenticated}
|
|
115
|
+
<main>
|
|
116
|
+
<AuthPanel config={authConfig} actions={authActions} {resetToken} initialMode={resetToken ? 'reset' : 'login'} onauthenticated={handleAuthenticated} />
|
|
117
|
+
</main>
|
|
118
|
+
{{/if}}
|
|
76
119
|
{:else}
|
|
77
120
|
<div class='hai-mobile-viewport'>
|
|
78
121
|
<div class='hai-mobile-shell max-w-lg'>
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
{{#if features.iam}}
|
|
2
|
+
import { mkdir } from 'node:fs/promises'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
{{/if}}
|
|
5
|
+
import process from 'node:process'
|
|
1
6
|
import { expect, test } from '@playwright/test'
|
|
2
7
|
|
|
3
8
|
/** 首页从真实浏览器页面发起,覆盖基础渲染链路。 */
|
|
@@ -9,3 +14,103 @@ test('home page renders from browser', async ({ page }) => {
|
|
|
9
14
|
await expect(pageErrors).toEqual([])
|
|
10
15
|
await expect(page.locator('main, section').first()).toBeVisible()
|
|
11
16
|
})
|
|
17
|
+
{{#if features.iam}}
|
|
18
|
+
|
|
19
|
+
/** 实际注册和密码登录;截图只保存空白表单。 */
|
|
20
|
+
test('configured IAM registration and password login', async ({ page, context }) => {
|
|
21
|
+
test.setTimeout(90_000)
|
|
22
|
+
const evidence = process.env.HAI_AUTH_EVIDENCE_DIR || path.resolve('test-results/auth-screenshots')
|
|
23
|
+
await mkdir(evidence, { recursive: true })
|
|
24
|
+
const username = `scaf${Date.now()}`
|
|
25
|
+
const password = 'ScaffoldPassword123'
|
|
26
|
+
const authUrl = '{{#if isCapacitorApp}}/{{else}}/auth/login{{/if}}'
|
|
27
|
+
await page.goto(authUrl)
|
|
28
|
+
await expect(page.locator('#login-username')).toBeVisible()
|
|
29
|
+
await page.screenshot({ path: path.join(evidence, '{{projectName}}-login.png'), fullPage: true })
|
|
30
|
+
await page.getByRole('{{#if isCapacitorApp}}button{{else}}link{{/if}}', { name: /立即注册|Register now/i }).click()
|
|
31
|
+
await expect(page.locator('input[name="username"]')).toBeVisible()
|
|
32
|
+
await page.screenshot({ path: path.join(evidence, '{{projectName}}-register.png'), fullPage: true })
|
|
33
|
+
if (process.env.IAM_NOTIFICATION_URL) {
|
|
34
|
+
// 协议弹窗留在认证页,关闭后继续填写注册表单。
|
|
35
|
+
await page.getByRole('button', { name: /用户协议|User Agreement/i, exact: true }).click()
|
|
36
|
+
await expect(page.getByRole('dialog')).toBeVisible()
|
|
37
|
+
await expect(page.getByRole('dialog').getByRole('heading', { name: 'Configured registration agreement' })).toBeVisible()
|
|
38
|
+
await expect(page.locator('iframe')).toHaveCount(0)
|
|
39
|
+
await page.screenshot({ path: path.join(evidence, '{{projectName}}-agreement.png'), fullPage: true })
|
|
40
|
+
await page.keyboard.press('Escape')
|
|
41
|
+
await expect(page.getByRole('dialog')).not.toBeVisible()
|
|
42
|
+
}
|
|
43
|
+
await page.locator('input[name="username"]').fill(username)
|
|
44
|
+
await page.locator('input[name="email"]').fill(`${username}@example.com`)
|
|
45
|
+
await page.locator('input[type="password"]').nth(0).fill(password)
|
|
46
|
+
await page.locator('input[type="password"]').nth(1).fill(password)
|
|
47
|
+
await page.locator('button[type="submit"]').click()
|
|
48
|
+
await expect(page.locator('.register-form')).not.toBeVisible()
|
|
49
|
+
{{#unless isCapacitorApp}}
|
|
50
|
+
const me = await page.request.get('/api/auth/me')
|
|
51
|
+
expect(me.ok()).toBe(true)
|
|
52
|
+
await context.clearCookies()
|
|
53
|
+
{{/unless}}
|
|
54
|
+
await page.reload()
|
|
55
|
+
await page.goto(authUrl)
|
|
56
|
+
await expect(page.locator('#login-username')).toBeVisible()
|
|
57
|
+
await page.locator('#login-username').fill(username)
|
|
58
|
+
await page.locator('input[type="password"]').fill(password)
|
|
59
|
+
await page.locator('button[type="submit"]').click()
|
|
60
|
+
await expect(page.locator('.login-form')).not.toBeVisible()
|
|
61
|
+
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
|
|
62
|
+
|
|
63
|
+
if (!process.env.IAM_NOTIFICATION_URL)
|
|
64
|
+
return
|
|
65
|
+
await context.clearCookies()
|
|
66
|
+
await page.reload()
|
|
67
|
+
await page.goto(authUrl)
|
|
68
|
+
await page.getByRole('button', { name: /邮件验证码|Email code/i, exact: true }).click()
|
|
69
|
+
await page.screenshot({ path: path.join(evidence, '{{projectName}}-email.png'), fullPage: true })
|
|
70
|
+
const email = `${username}@example.com`
|
|
71
|
+
await page.locator('#login-username').fill(email)
|
|
72
|
+
await page.getByRole('button', { name: /发送验证码|Send code/i, exact: true }).click()
|
|
73
|
+
await expect(page.getByRole('button', { name: /秒后重发|Resend in/ })).toBeDisabled()
|
|
74
|
+
async function received(template: string) {
|
|
75
|
+
const response = await page.request.get(`${process.env.IAM_NOTIFICATION_URL}/messages?to=${encodeURIComponent(email)}`)
|
|
76
|
+
const messages = await response.json() as { template: string, vars: Record<string, string> }[]
|
|
77
|
+
return messages.findLast(item => item.template === template)?.vars
|
|
78
|
+
}
|
|
79
|
+
const otp = await received('otp_email')
|
|
80
|
+
expect(Boolean(otp?.code)).toBe(true)
|
|
81
|
+
await page.locator('#login-code').fill(otp?.code ?? '')
|
|
82
|
+
await page.locator('button[type="submit"]').click()
|
|
83
|
+
await expect(page.locator('.login-form')).not.toBeVisible()
|
|
84
|
+
await context.clearCookies()
|
|
85
|
+
await page.reload()
|
|
86
|
+
await page.goto(authUrl)
|
|
87
|
+
await page.getByRole('button', { name: /忘记密码|Forgot password/i }).click()
|
|
88
|
+
await page.screenshot({ path: path.join(evidence, '{{projectName}}-forgot.png'), fullPage: true })
|
|
89
|
+
await page.locator('input[type="email"]').fill(email)
|
|
90
|
+
await page.locator('button[type="submit"]').click()
|
|
91
|
+
await expect(page.locator('.alert-success')).toBeVisible()
|
|
92
|
+
const reset = await received('password_reset')
|
|
93
|
+
expect(Boolean(reset?.token)).toBe(true)
|
|
94
|
+
await page.getByRole('button', { name: /已有重置令牌|I have a reset token/i }).click()
|
|
95
|
+
await page.screenshot({ path: path.join(evidence, '{{projectName}}-reset.png'), fullPage: true })
|
|
96
|
+
await page.goto(`{{#if isCapacitorApp}}/{{else}}/auth/reset-password{{/if}}?token=${encodeURIComponent(reset?.token ?? '')}`)
|
|
97
|
+
await expect(page.locator('input[type="password"]').nth(0)).toBeVisible()
|
|
98
|
+
await page.locator('input[type="password"]').nth(0).fill('ScaffoldReset456')
|
|
99
|
+
await page.locator('input[type="password"]').nth(1).fill('ScaffoldReset456')
|
|
100
|
+
await page.locator('button[type="submit"]').click()
|
|
101
|
+
await expect(page.locator('.alert-success')).toBeVisible()
|
|
102
|
+
await page.getByRole('{{#if isCapacitorApp}}button{{else}}link{{/if}}', { name: /^(登录|Sign in|Login)$/i }).click()
|
|
103
|
+
await page.locator('#login-username').fill(username)
|
|
104
|
+
await page.locator('input[type="password"]').fill('ScaffoldReset456')
|
|
105
|
+
await page.locator('button[type="submit"]').click()
|
|
106
|
+
await expect(page.locator('.login-form')).not.toBeVisible()
|
|
107
|
+
expect(new URL(page.url()).searchParams.has('token')).toBe(false)
|
|
108
|
+
await page.reload()
|
|
109
|
+
{{#if isCapacitorApp}}
|
|
110
|
+
// Web 预览采用内存 TokenStore,刷新后应回到登录,而不是已消费令牌的重置页面。
|
|
111
|
+
await expect(page.locator('#login-username')).toBeVisible()
|
|
112
|
+
{{else}}
|
|
113
|
+
await expect(await page.request.get('/api/auth/me')).toBeOK()
|
|
114
|
+
{{/if}}
|
|
115
|
+
})
|
|
116
|
+
{{/if}}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"engines": { "node": "^22.22.2 || ^24.15.0 || >=26.0.0" },
|
|
3
3
|
"name": "{{projectName}}",
|
|
4
|
-
"version": "0.1.0-alpha.
|
|
4
|
+
"version": "0.1.0-alpha.60",
|
|
5
5
|
"private": true,
|
|
6
6
|
"packageManager": "pnpm@12.11.2",
|
|
7
7
|
"description": "{{#if_eq appType 'admin'}}Enterprise management system built with hai Agent Framework{{/if_eq}}{{#if_eq appType 'website'}}Enterprise website built with hai Agent Framework{{/if_eq}}{{#if_eq appType 'h5'}}Mobile H5 app built with hai Agent Framework{{/if_eq}}{{#if_eq appType 'api'}}REST API service built with hai Agent Framework{{/if_eq}}{{#if_eq appType 'mobile-app'}}Mobile app built with hai Agent Framework{{/if_eq}}",
|