@h-ai/cli 1.0.0-beta.17 → 1.0.0-beta.19
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 +6 -0
- package/README.ja.md +6 -0
- package/README.md +6 -0
- package/dist/{chunk-EWTIVFHN.js → chunk-2ZL5ORQ2.js} +94 -28
- package/dist/chunk-2ZL5ORQ2.js.map +1 -0
- package/dist/{chunk-S2ZP7BHG.js → chunk-L53P43B2.js} +138 -32
- package/dist/chunk-L53P43B2.js.map +1 -0
- package/dist/cli-config-templates-LWMD2I6V.js +5 -0
- package/dist/{cli-config-templates-MKVYSK3E.js.map → cli-config-templates-LWMD2I6V.js.map} +1 -1
- package/dist/{cli-skill-templates-M64VPXZ4.js → cli-skill-templates-D6S2OQ6X.js} +3 -3
- package/dist/{cli-skill-templates-M64VPXZ4.js.map → cli-skill-templates-D6S2OQ6X.js.map} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2797 -666
- package/dist/index.js.map +1 -1
- package/dist/metafile-esm.json +1 -1
- package/package.json +22 -22
- package/templates/apps/admin/src/routes/+layout.svelte.hbs +16 -10
- package/templates/apps/admin/src/routes/+page.svelte.hbs +4 -4
- package/templates/apps/admin/src/routes/admin/+layout.svelte.hbs +17 -7
- package/templates/apps/admin/src/routes/admin/+page.svelte.hbs +26 -6
- package/templates/apps/admin/src/routes/admin/settings/+page.svelte.hbs +2 -2
- package/templates/apps/api/apps/{{projectName}}-contract/src/app-schemas.ts.hbs +6 -1
- package/templates/apps/api/apps/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +13 -5
- package/templates/apps/api/apps/{{projectName}}-contract/tests/contract.test.ts.hbs +13 -5
- package/templates/apps/api/apps/{{projectName}}-contract/tsup.config.ts.hbs +3 -1
- package/templates/apps/api/apps/{{projectName}}-serv/src/app.ts.hbs +71 -32
- package/templates/apps/api/apps/{{projectName}}-serv/src/build-metadata.ts.hbs +0 -1
- package/templates/apps/api/apps/{{projectName}}-serv/src/lib/server/init.ts.hbs +131 -54
- package/templates/apps/api/apps/{{projectName}}-serv/src/server/procedures/index.ts.hbs +39 -13
- package/templates/apps/api/apps/{{projectName}}-serv/tests/service.test.ts.hbs +78 -31
- package/templates/apps/api/apps/{{projectName}}-serv/tsup.config.ts.hbs +3 -1
- package/templates/apps/api/apps/{{projectName}}-serv/vitest.config.ts.hbs +3 -1
- package/templates/apps/api/e2e/api-serv.spec.ts.hbs +20 -3
- package/templates/apps/api/e2e/capture-evidence.ts.hbs +4 -1
- package/templates/apps/api/eslint.config.js.hbs +3 -1
- package/templates/apps/api/images/serv.Dockerfile.hbs +3 -4
- package/templates/apps/api/playwright.config.ts.hbs +23 -19
- package/templates/apps/api/pnpm-workspace.yaml.hbs +27 -16
- package/templates/apps/fullstack/apps/{{projectName}}-app/capacitor.config.ts.hbs +1 -3
- package/templates/apps/fullstack/apps/{{projectName}}-app/eslint.config.js.hbs +10 -2
- package/templates/apps/fullstack/apps/{{projectName}}-app/src/App.svelte.hbs +10 -2
- package/templates/apps/fullstack/apps/{{projectName}}-app/src/build-metadata.ts.hbs +0 -1
- package/templates/apps/fullstack/apps/{{projectName}}-app/src/lib/capacitor.ts.hbs +0 -1
- package/templates/apps/fullstack/apps/{{projectName}}-app/svelte.config.js.hbs +4 -1
- package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +16 -4
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/App.svelte.hbs +8 -2
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/build-metadata.ts.hbs +0 -1
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/svelte.config.js.hbs +4 -1
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +16 -4
- package/templates/apps/fullstack/apps/{{projectName}}-web/eslint.config.js.hbs +10 -2
- package/templates/apps/fullstack/apps/{{projectName}}-web/src/build-metadata.ts.hbs +0 -1
- package/templates/apps/fullstack/apps/{{projectName}}-web/src/main.ts.hbs +0 -2
- package/templates/apps/fullstack/apps/{{projectName}}-web/svelte.config.js.hbs +4 -1
- package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +19 -5
- package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +111 -26
- package/templates/apps/fullstack/e2e/session-close.spec.ts.hbs +19 -5
- package/templates/apps/fullstack/eslint.config.js.hbs +3 -1
- package/templates/apps/fullstack/images/serv.Dockerfile.hbs +3 -4
- package/templates/apps/fullstack/images/web.Dockerfile.hbs +0 -2
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-contract.ts.hbs +114 -23
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-schemas.ts.hbs +57 -11
- package/templates/apps/fullstack/packages/{{projectName}}-contract/tsup.config.ts.hbs +3 -1
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/app-server.ts.hbs +65 -30
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/build-metadata.ts.hbs +0 -1
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/admin-service.ts.hbs +69 -21
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/business-settings.ts.hbs +21 -5
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/admin-procedures.ts.hbs +63 -11
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/app-procedures.ts.hbs +6 -1
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/index.ts.hbs +3 -1
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/runtime.ts.hbs +139 -49
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/security-roles.ts.hbs +43 -12
- package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/app-server.test.ts.hbs +139 -28
- package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/runtime.test.ts.hbs +33 -10
- package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/security.test.ts.hbs +141 -25
- package/templates/apps/fullstack/packages/{{projectName}}-serv/tsup.config.ts.hbs +6 -2
- package/templates/apps/fullstack/packages/{{projectName}}-serv/vitest.config.ts.hbs +3 -1
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/api-client.ts.hbs +9 -2
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/feedback-client.ts.hbs +0 -3
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/sensitive-actions.svelte.ts.hbs +15 -3
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +33 -7
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/FeedbackEntry.svelte.hbs +16 -2
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/audit.svelte.hbs +104 -13
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/feedback.svelte.hbs +37 -7
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/roles.svelte.hbs +9 -3
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/settings.svelte.hbs +25 -3
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/users.svelte.hbs +133 -19
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/security/role-label.ts.hbs +14 -2
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/guide-store.ts.hbs +66 -34
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/management-store.svelte.ts.hbs +132 -30
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/theme-store.svelte.ts.hbs +8 -2
- package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/api-client.test.ts.hbs +23 -5
- package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/management-store.test.ts.hbs +143 -21
- package/templates/apps/fullstack/packages/{{projectName}}-shared/vitest.config.ts.hbs +7 -2
- package/templates/apps/fullstack/playwright.config.ts.hbs +9 -3
- package/templates/apps/fullstack/pnpm-workspace.yaml.hbs +30 -15
- package/templates/apps/h5/src/routes/+layout.svelte.hbs +57 -21
- package/templates/apps/h5/src/routes/+page.svelte.hbs +2 -2
- package/templates/apps/h5/src/routes/discover/+page.svelte.hbs +2 -2
- package/templates/apps/h5/src/routes/profile/+page.svelte.hbs +2 -2
- package/templates/apps/mobile-app/src/App.svelte.hbs +17 -5
- package/templates/apps/mobile-app/src/lib/api.ts.hbs +14 -3
- package/templates/apps/mobile-app/src/lib/app-config.ts.hbs +8 -1
- package/templates/apps/mobile-app/src/lib/pages/home.svelte.hbs +6 -2
- package/templates/apps/mobile-app/src/lib/pages/tasks.svelte.hbs +5 -1
- package/templates/apps/website/scripts/static-website.mjs.hbs +16 -3
- package/templates/apps/website/src/routes/+layout.svelte.hbs +44 -23
- package/templates/apps/website/src/routes/+page.svelte.hbs +4 -4
- package/templates/apps/website/src/routes/about/+page.svelte.hbs +2 -2
- package/templates/apps/website/src/routes/contact/+page.svelte.hbs +2 -2
- package/templates/apps/website/src/routes/services/+page.svelte.hbs +20 -5
- package/templates/base/e2e/app.spec.ts.hbs +49 -16
- package/templates/base/eslint-rules.js +44 -25
- package/templates/base/eslint.config.js.hbs +3 -1
- package/templates/base/images/web.Dockerfile.hbs +3 -4
- package/templates/base/package.json.hbs +5 -4
- package/templates/base/patches/daisyui-5.7.50.patch +8 -0
- package/templates/base/patches/deepmerge-4.3.1.patch +51 -0
- package/templates/base/playwright.config.ts +3 -1
- package/templates/base/pnpm-workspace.yaml.hbs +33 -16
- package/templates/base/src/app.d.ts.hbs +1 -1
- package/templates/base/src/hooks.server.ts.hbs +36 -27
- package/templates/base/src/lib/build-metadata.ts.hbs +0 -1
- package/templates/base/src/lib/server/init.ts.hbs +124 -51
- package/templates/base/src/lib/server/kit-config.ts.hbs +7 -4
- package/templates/base/src/routes/+layout.server.ts.hbs +2 -2
- package/templates/base/svelte.config.js.hbs +10 -100
- package/templates/base/tests/scaffold.test.ts.hbs +12 -3
- package/templates/base/tsconfig.json.hbs +7 -4
- package/templates/base/vite.config.ts.hbs +108 -14
- package/templates/base/vitest.config.ts.hbs +6 -2
- package/templates/features/ai/routes/api/chat/+server.ts +3 -1
- package/templates/features/iam/routes-admin/(auth)/auth/forgot-password/+page.svelte.hbs +3 -3
- package/templates/features/iam/routes-admin/(auth)/auth/login/+page.svelte.hbs +3 -3
- package/templates/features/iam/routes-admin/(auth)/auth/register/+page.svelte.hbs +3 -3
- package/templates/features/iam/routes-admin/(auth)/auth/reset-password/+page.svelte.hbs +3 -3
- package/templates/features/iam/routes-h5/auth/forgot-password/+page.svelte.hbs +3 -3
- package/templates/features/iam/routes-h5/auth/login/+page.svelte.hbs +3 -3
- package/templates/features/iam/routes-h5/auth/register/+page.svelte.hbs +3 -3
- package/templates/features/iam/routes-h5/auth/reset-password/+page.svelte.hbs +3 -3
- package/templates/features/iam/routes-website/auth/forgot-password/+page.svelte.hbs +3 -3
- package/templates/features/iam/routes-website/auth/login/+page.svelte.hbs +3 -3
- package/templates/features/iam/routes-website/auth/register/+page.svelte.hbs +3 -3
- package/templates/features/iam/routes-website/auth/reset-password/+page.svelte.hbs +3 -3
- package/templates/features/payment/routes/api/v1/payment/create/+server.ts +11 -2
- package/templates/shared/browser-check/startup.js +32 -6
- package/templates/shared/browser-startup.spec.ts +3 -1
- package/templates/shared/build-metadata.ts +18 -4
- package/templates/shared/e2e-env.ts +22 -6
- package/templates/shared/native-name.mjs +9 -2
- package/templates/shared/public-base-path.mjs +17 -4
- package/templates/shared/run-e2e.mjs +40 -9
- package/templates/shared/run-node.mjs +15 -4
- package/templates/shared/run-tauri.mjs +37 -8
- package/templates/shared/vite-runtime-config.ts +9 -3
- package/templates/skills/AGENTS.md +10 -73
- package/templates/skills/bridges/admin/AGENTS.md +9 -65
- package/templates/skills/bridges/api/AGENTS.md +8 -59
- package/templates/skills/bridges/fullstack/AGENTS.md.hbs +9 -54
- package/templates/skills/bridges/h5/AGENTS.md +8 -63
- package/templates/skills/bridges/mobile-app/AGENTS.md +8 -62
- package/templates/skills/bridges/website/AGENTS.md +8 -63
- package/templates/skills/hai-ai/acp.md +9 -1
- package/templates/skills/hai-ai/guide.md +17 -6
- package/templates/skills/hai-ai/reference.md +21 -12
- package/templates/skills/hai-api-client/reference.md +1 -1
- package/templates/skills/hai-app-create/development.md +3 -1
- package/templates/skills/hai-app-create/project.md +33 -0
- package/templates/skills/hai-app-create/reference.md +8 -8
- package/templates/skills/hai-app-tests/reference.md +1 -1
- package/templates/skills/hai-build/SKILL.md +3 -2
- package/templates/skills/hai-build/paths.md +1 -1
- package/templates/skills/hai-build/reference.md +11 -7
- package/templates/skills/hai-capacitor/reference.md +2 -0
- package/templates/skills/hai-ci/SKILL.md +1 -0
- package/templates/skills/hai-core/reference.md +3 -0
- package/templates/skills/hai-crypto/reference.md +2 -0
- package/templates/skills/hai-deploy/SKILL.md +7 -1
- package/templates/skills/hai-deploy/middleware.md +3 -1
- package/templates/skills/hai-framework-sync/SKILL.md +13 -9
- package/templates/skills/hai-framework-sync/sync.md +2 -0
- package/templates/skills/hai-kit/reference.md +16 -4
- package/templates/skills/hai-payment/reference.md +2 -0
- package/templates/skills/hai-reach/SKILL.md +2 -1
- package/templates/skills/hai-reach/reference.md +1 -1
- package/templates/skills/hai-reldb/SKILL.md +1 -1
- package/templates/skills/hai-reldb/reference.md +4 -0
- package/templates/skills/hai-serv/reference.md +12 -2
- package/templates/skills/hai-storage/reference.md +12 -3
- package/templates/skills/hai-ui/SKILL.md +3 -2
- package/templates/skills/hai-ui/guide.md +1 -1
- package/templates/skills/hai-ui/reference.md +23 -35
- package/templates/skills/hai-vecdb/reference.md +1 -1
- package/dist/chunk-EWTIVFHN.js.map +0 -1
- package/dist/chunk-S2ZP7BHG.js.map +0 -1
- package/dist/cli-config-templates-MKVYSK3E.js +0 -5
|
@@ -35,7 +35,13 @@ async function capture(page: Page, name: string, fallback: string) {
|
|
|
35
35
|
await mkdir(evidence, { recursive: true })
|
|
36
36
|
await expect(page.getByTestId('guide-loading')).toHaveCount(0)
|
|
37
37
|
await page.evaluate(() => document.fonts.ready)
|
|
38
|
-
await page.screenshot({
|
|
38
|
+
await page.screenshot({
|
|
39
|
+
path: evidence
|
|
40
|
+
? path.join(evidence, name)
|
|
41
|
+
: fallback,
|
|
42
|
+
fullPage: true,
|
|
43
|
+
animations: 'disabled',
|
|
44
|
+
})
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
async function dismissToasts(page: Page) {
|
|
@@ -62,13 +68,18 @@ async function login(page: Page, username: string, password: string, keepGuide =
|
|
|
62
68
|
async function userEditor(page: Page, username: string) {
|
|
63
69
|
await page.getByTestId('users-search').locator('input').fill(username)
|
|
64
70
|
await page.getByTestId('users-page').getByRole('button', { name: /查询|Search/ }).click()
|
|
65
|
-
const row = page.getByRole('row').filter({
|
|
71
|
+
const row = page.getByRole('row').filter({
|
|
72
|
+
has: page.getByText(`@${username}`, { exact: true }),
|
|
73
|
+
})
|
|
66
74
|
await expect(row).toBeVisible()
|
|
67
75
|
await row.getByRole('button', { name: /编辑|Edit/, exact: true }).click()
|
|
68
76
|
return row
|
|
69
77
|
}
|
|
70
78
|
|
|
71
|
-
for (const scenario of [
|
|
79
|
+
for (const scenario of [
|
|
80
|
+
{ offset: -600_000, width: 1440 },
|
|
81
|
+
{ offset: 600_000, width: 390 },
|
|
82
|
+
]) {
|
|
72
83
|
test(`clock offset ${scenario.offset} displays synchronization advice and recovers after correction`, async ({ page }, testInfo) => {
|
|
73
84
|
await page.setViewportSize({ width: scenario.width, height: 900 })
|
|
74
85
|
await page.addInitScript((offset) => {
|
|
@@ -99,10 +110,11 @@ test('service health endpoint is ready', async ({ request }) => {
|
|
|
99
110
|
})
|
|
100
111
|
|
|
101
112
|
test('service rejects plaintext business requests', async ({ request }) => {
|
|
102
|
-
const response = await request.post(`${apiBaseUrl}${apiPrefix}${apiContract.pathOf({{fullstack.contractExportName}}.app.echo)}`, {
|
|
113
|
+
const response = await request.post(`${apiBaseUrl}${apiPrefix}${apiContract.pathOf({{fullstack.contractExportName}}.app.echo)}`, {
|
|
114
|
+
data: { message: 'hello e2e' },
|
|
115
|
+
})
|
|
103
116
|
expect(response.status()).toBe(400)
|
|
104
117
|
})
|
|
105
|
-
|
|
106
118
|
{{#if fullstack.e2eFrontend}}
|
|
107
119
|
test('hash routing and responsive workspace preserve usable content width', async ({ page }, testInfo) => {
|
|
108
120
|
await page.goto('./#/roles?source=direct')
|
|
@@ -289,7 +301,11 @@ const additionalFrontends = [
|
|
|
289
301
|
|
|
290
302
|
for (const frontend of additionalFrontends) {
|
|
291
303
|
test(`${frontend.target} preview supports login, tour, help and profile navigation`, async ({ browser }, testInfo) => {
|
|
292
|
-
const page = await browser.newPage({
|
|
304
|
+
const page = await browser.newPage({
|
|
305
|
+
viewport: frontend.target === 'app'
|
|
306
|
+
? { width: 390, height: 844 }
|
|
307
|
+
: { width: 1440, height: 960 },
|
|
308
|
+
})
|
|
293
309
|
const pageErrors: string[] = []
|
|
294
310
|
page.on('pageerror', error => pageErrors.push(error.message))
|
|
295
311
|
try {
|
|
@@ -329,11 +345,17 @@ test('shared authentication supports public registration with configuration', as
|
|
|
329
345
|
await mkdir(evidence, { recursive: true })
|
|
330
346
|
await page.goto('./')
|
|
331
347
|
await expect(page.getByTestId('login-form')).toBeVisible()
|
|
332
|
-
await page.screenshot({
|
|
348
|
+
await page.screenshot({
|
|
349
|
+
path: path.join(evidence, '{{projectName}}-login.png'),
|
|
350
|
+
fullPage: true,
|
|
351
|
+
})
|
|
333
352
|
await page.getByRole('button', { name: /立即注册|Register now/i }).click()
|
|
334
353
|
const registerForm = page.locator('.register-form')
|
|
335
354
|
await expect(registerForm.locator('#register-username')).toBeVisible()
|
|
336
|
-
await page.screenshot({
|
|
355
|
+
await page.screenshot({
|
|
356
|
+
path: path.join(evidence, '{{projectName}}-register.png'),
|
|
357
|
+
fullPage: true,
|
|
358
|
+
})
|
|
337
359
|
const username = `signup${randomUUID().replaceAll('-', '').slice(0, 12)}`
|
|
338
360
|
await registerForm.locator('#register-username').fill(username)
|
|
339
361
|
await registerForm.locator('#register-email').fill(`${username}@example.com`)
|
|
@@ -347,7 +369,10 @@ test('shared authentication supports public registration with configuration', as
|
|
|
347
369
|
const email = `${username}@example.com`
|
|
348
370
|
await page.getByTestId('logout').click()
|
|
349
371
|
await page.getByRole('button', { name: /邮件验证码|Email code/i, exact: true }).click()
|
|
350
|
-
await page.screenshot({
|
|
372
|
+
await page.screenshot({
|
|
373
|
+
path: path.join(evidence, '{{projectName}}-email.png'),
|
|
374
|
+
fullPage: true,
|
|
375
|
+
})
|
|
351
376
|
await page.locator('#login-username').fill(email)
|
|
352
377
|
await page.getByRole('button', { name: /发送验证码|Send code/i, exact: true }).click()
|
|
353
378
|
await expect(page.getByRole('button', { name: /秒后重发|Resend in/ })).toBeDisabled()
|
|
@@ -363,14 +388,20 @@ test('shared authentication supports public registration with configuration', as
|
|
|
363
388
|
await expect(page.getByTestId('settings-page')).toBeVisible()
|
|
364
389
|
await page.getByTestId('logout').click()
|
|
365
390
|
await page.getByRole('button', { name: /忘记密码|Forgot password/i }).click()
|
|
366
|
-
await page.screenshot({
|
|
391
|
+
await page.screenshot({
|
|
392
|
+
path: path.join(evidence, '{{projectName}}-forgot.png'),
|
|
393
|
+
fullPage: true,
|
|
394
|
+
})
|
|
367
395
|
await page.locator('input[type="email"]').fill(email)
|
|
368
396
|
await page.locator('button[type="submit"]').click()
|
|
369
397
|
await expect(page.locator('.hai-toast-item[data-variant="success"]')).toBeVisible()
|
|
370
398
|
const reset = await received('password_reset')
|
|
371
399
|
expect(Boolean(reset?.token)).toBe(true)
|
|
372
400
|
await page.getByRole('button', { name: /已有重置令牌|I have a reset token/i }).click()
|
|
373
|
-
await page.screenshot({
|
|
401
|
+
await page.screenshot({
|
|
402
|
+
path: path.join(evidence, '{{projectName}}-reset.png'),
|
|
403
|
+
fullPage: true,
|
|
404
|
+
})
|
|
374
405
|
await page.goto(`./?token=${encodeURIComponent(reset?.token ?? '')}`)
|
|
375
406
|
await expect(page.locator('input[type="password"]').nth(0)).toBeVisible()
|
|
376
407
|
await page.locator('input[type="password"]').nth(0).fill('FullstackReset456')
|
|
@@ -401,7 +432,12 @@ test('frontend displays configured core metadata and restores the administrator
|
|
|
401
432
|
const evidence = process.env.HAI_AUTH_EVIDENCE_DIR
|
|
402
433
|
if (evidence)
|
|
403
434
|
await mkdir(evidence, { recursive: true })
|
|
404
|
-
await page.screenshot({
|
|
435
|
+
await page.screenshot({
|
|
436
|
+
path: evidence
|
|
437
|
+
? path.join(evidence, '{{projectName}}-users.png')
|
|
438
|
+
: testInfo.outputPath('users.png'),
|
|
439
|
+
fullPage: true,
|
|
440
|
+
})
|
|
405
441
|
await page.getByTestId('logout').click()
|
|
406
442
|
await expect(page.getByTestId('login-form')).toBeVisible()
|
|
407
443
|
})
|
|
@@ -524,9 +560,13 @@ test('audit filters collapse without losing values and fit compact workspaces',
|
|
|
524
560
|
await page.setViewportSize({ width, height: 900 })
|
|
525
561
|
if (width < 1024)
|
|
526
562
|
await expect.poll(() => page.locator('aside').evaluate(sidebar => sidebar.getBoundingClientRect().right)).toBeLessThanOrEqual(1)
|
|
527
|
-
const controls = await page.getByTestId('audit-advanced').evaluate(panel => [
|
|
563
|
+
const controls = await page.getByTestId('audit-advanced').evaluate(panel => [
|
|
564
|
+
...panel.querySelectorAll('input'),
|
|
565
|
+
].map((element) => {
|
|
528
566
|
// 复合控件的点击框包含边框和留白,内部文字输入区不等于完整控件高度。
|
|
529
|
-
const control = element.closest('.fieldset')
|
|
567
|
+
const control = element.closest('.fieldset')
|
|
568
|
+
? element.parentElement!
|
|
569
|
+
: element
|
|
530
570
|
const rect = control.getBoundingClientRect()
|
|
531
571
|
return { left: rect.left, right: rect.right, height: rect.height }
|
|
532
572
|
}))
|
|
@@ -542,7 +582,13 @@ test('audit filters collapse without losing values and fit compact workspaces',
|
|
|
542
582
|
if (evidence)
|
|
543
583
|
await mkdir(evidence, { recursive: true })
|
|
544
584
|
const file = `{{projectName}}-audit-${width}.png`
|
|
545
|
-
await page.screenshot({
|
|
585
|
+
await page.screenshot({
|
|
586
|
+
path: evidence
|
|
587
|
+
? path.join(evidence, file)
|
|
588
|
+
: testInfo.outputPath(file),
|
|
589
|
+
fullPage: true,
|
|
590
|
+
animations: 'disabled',
|
|
591
|
+
})
|
|
546
592
|
}
|
|
547
593
|
}
|
|
548
594
|
const reset = page.waitForResponse(response => new URL(response.url()).pathname.endsWith('/admin/audit/list'))
|
|
@@ -563,7 +609,11 @@ test('audit filters collapse without losing values and fit compact workspaces',
|
|
|
563
609
|
await toggle.click()
|
|
564
610
|
await page.getByTestId('audit-page').getByRole('button', { name: /详情|Details/ }).first().click()
|
|
565
611
|
await expect(page.getByRole('dialog')).toBeVisible()
|
|
566
|
-
await page.screenshot({
|
|
612
|
+
await page.screenshot({
|
|
613
|
+
path: testInfo.outputPath('audit-short-table-details.png'),
|
|
614
|
+
fullPage: true,
|
|
615
|
+
animations: 'disabled',
|
|
616
|
+
})
|
|
567
617
|
})
|
|
568
618
|
|
|
569
619
|
test('administrator manages every duty through one lifecycle and the shared password reset form', async ({ page, browser }, testInfo) => {
|
|
@@ -718,8 +768,16 @@ test('administrator and user complete account, avatar, preference, password and
|
|
|
718
768
|
await userPage.getByRole('option', { name: 'English', exact: true }).click()
|
|
719
769
|
await expect(userPage.getByTestId('nav-settings')).toHaveText('Personal settings')
|
|
720
770
|
await expect(userPage.locator('html')).toHaveAttribute('lang', 'en-US')
|
|
721
|
-
const themes = [
|
|
722
|
-
|
|
771
|
+
const themes = [
|
|
772
|
+
{{#each fullstack.theme.themes}}
|
|
773
|
+
'{{this}}',
|
|
774
|
+
{{/each}}
|
|
775
|
+
]
|
|
776
|
+
const selectedTheme = themes.includes('formal')
|
|
777
|
+
? 'formal'
|
|
778
|
+
: themes.includes('high-contrast')
|
|
779
|
+
? 'high-contrast'
|
|
780
|
+
: themes.find(theme => theme !== '{{fullstack.theme.defaultTheme}}') ?? themes[0]
|
|
723
781
|
await userPage.getByTestId('profile-theme').click()
|
|
724
782
|
await userPage.getByRole('option').nth(themes.indexOf(selectedTheme)).click()
|
|
725
783
|
await expect(userPage.locator('html')).toHaveAttribute('data-theme', selectedTheme)
|
|
@@ -738,7 +796,10 @@ test('administrator and user complete account, avatar, preference, password and
|
|
|
738
796
|
await expect(userPage.locator('html')).toHaveAttribute('data-theme', 'eye-care')
|
|
739
797
|
await expect.poll(() => userPage.evaluate(() => getComputedStyle(document.documentElement).colorScheme)).toBe('light')
|
|
740
798
|
await expect.poll(() => userPage.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue('--color-base-100').trim())).toBe('#e8f0df')
|
|
741
|
-
await userPage.screenshot({
|
|
799
|
+
await userPage.screenshot({
|
|
800
|
+
path: testInfo.outputPath('fullstack-eye-care.png'),
|
|
801
|
+
animations: 'disabled',
|
|
802
|
+
})
|
|
742
803
|
}
|
|
743
804
|
await expect(userPage.getByTestId('profile-form').locator('input[name="displayName"]')).toHaveValue('Updated member')
|
|
744
805
|
await expect(userPage.getByTestId('profile-form').locator('input[name="email"]')).toHaveValue('')
|
|
@@ -868,7 +929,11 @@ test('feedback submits an empty description and business operator can process an
|
|
|
868
929
|
await dialog.locator('input[type=file]').setInputFiles([
|
|
869
930
|
{ name: 'attached-preview.png', mimeType: 'image/png', buffer: feedbackImage },
|
|
870
931
|
{ name: 'attached-large-log.txt', mimeType: 'text/plain', buffer: feedbackLog },
|
|
871
|
-
{
|
|
932
|
+
{
|
|
933
|
+
name: 'discarded.txt',
|
|
934
|
+
mimeType: 'text/plain',
|
|
935
|
+
buffer: Buffer.from('discarded local material'),
|
|
936
|
+
},
|
|
872
937
|
])
|
|
873
938
|
const uploaded = dialog.locator('img[alt="attached-preview.png"]')
|
|
874
939
|
await expect(uploaded).toBeVisible()
|
|
@@ -876,7 +941,10 @@ test('feedback submits an empty description and business operator can process an
|
|
|
876
941
|
await expectDialogViewport()
|
|
877
942
|
await expectUncovered(uploaded)
|
|
878
943
|
await capture(page, 'feedback-image-before-mask.png', testInfo.outputPath('feedback-image-before-mask.png'))
|
|
879
|
-
await uploaded.screenshot({
|
|
944
|
+
await uploaded.screenshot({
|
|
945
|
+
path: testInfo.outputPath('feedback-image-before-mask-detail.png'),
|
|
946
|
+
animations: 'disabled',
|
|
947
|
+
})
|
|
880
948
|
const originalPreview = await uploaded.getAttribute('src')
|
|
881
949
|
await dialog.getByRole('button', { name: /遮挡敏感区域|Mask sensitive area|機密情報を隠す/ }).last().click()
|
|
882
950
|
await dialog.getByRole('button', { name: /应用标注|Apply annotation|注釈を適用/ }).last().click()
|
|
@@ -901,7 +969,10 @@ test('feedback submits an empty description and business operator can process an
|
|
|
901
969
|
await expectDialogViewport()
|
|
902
970
|
await expectUncovered(uploaded)
|
|
903
971
|
await capture(page, 'feedback-preview.png', testInfo.outputPath('feedback-preview.png'))
|
|
904
|
-
await uploaded.screenshot({
|
|
972
|
+
await uploaded.screenshot({
|
|
973
|
+
path: testInfo.outputPath('feedback-image-after-mask-detail.png'),
|
|
974
|
+
animations: 'disabled',
|
|
975
|
+
})
|
|
905
976
|
// 回执只表示记录已受理;等这份大附件真正上传成功,再开始后台处理操作。
|
|
906
977
|
const largeUpload = page.waitForResponse(response => response.request().method() === 'POST'
|
|
907
978
|
&& /\/feedback\/reports\/[^/]+\/attachments\/[^/]+$/.test(new URL(response.url()).pathname)
|
|
@@ -972,7 +1043,12 @@ test('feedback submits an empty description and business operator can process an
|
|
|
972
1043
|
const serverManifest: { version: string } = JSON.parse(await readFile(path.resolve('packages/{{projectName}}-serv/package.json'), 'utf8'))
|
|
973
1044
|
let expectedCommit = 'unknown'
|
|
974
1045
|
try {
|
|
975
|
-
execFileSync('git', [
|
|
1046
|
+
execFileSync('git', [
|
|
1047
|
+
'ls-files',
|
|
1048
|
+
'--error-unmatch',
|
|
1049
|
+
'--',
|
|
1050
|
+
'package.json',
|
|
1051
|
+
], { stdio: 'pipe', windowsHide: true })
|
|
976
1052
|
expectedCommit = execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8', stdio: 'pipe', windowsHide: true }).trim()
|
|
977
1053
|
}
|
|
978
1054
|
catch { /* 无 Git 的源码归档必须明确保留 unknown,不冒用父工程提交。 */ }
|
|
@@ -981,7 +1057,10 @@ test('feedback submits an empty description and business operator can process an
|
|
|
981
1057
|
expect(expectedCommit).toMatch(/^(?:[a-f\d]{40}|[a-f\d]{64})$/i)
|
|
982
1058
|
expect(browserManifest.version).not.toBe(serverManifest.version)
|
|
983
1059
|
}
|
|
984
|
-
for (const [metadata, version] of [
|
|
1060
|
+
for (const [metadata, version] of [
|
|
1061
|
+
[browserMetadata, browserManifest.version],
|
|
1062
|
+
[serverMetadata, serverManifest.version],
|
|
1063
|
+
] as const) {
|
|
985
1064
|
expect(Object.keys(metadata).sort()).toEqual(['appVersion', 'buildId', 'commit'])
|
|
986
1065
|
expect(metadata.appVersion).toBe(version)
|
|
987
1066
|
expect(metadata.buildId).toMatch(/^[\w.+-]{1,160}$/)
|
|
@@ -993,7 +1072,10 @@ test('feedback submits an empty description and business operator can process an
|
|
|
993
1072
|
expect(browserEnvironment?.data).toMatchObject(browserMetadata)
|
|
994
1073
|
expect(serverEnvironment?.data.service).toMatchObject(serverMetadata)
|
|
995
1074
|
expect(bundle.report).toMatchObject(browserMetadata)
|
|
996
|
-
await testInfo.attach('feedback-build-metadata', {
|
|
1075
|
+
await testInfo.attach('feedback-build-metadata', {
|
|
1076
|
+
body: JSON.stringify({ browser: browserMetadata, server: serverMetadata }),
|
|
1077
|
+
contentType: 'application/json',
|
|
1078
|
+
})
|
|
997
1079
|
expect(bundle.report.number).toBe(number)
|
|
998
1080
|
expect(bundle.manifest.reportId).toBe(bundle.report.id)
|
|
999
1081
|
expect(bundle.manifest.evidenceRevision).toBe(bundle.report.evidenceRevision)
|
|
@@ -1009,7 +1091,10 @@ test('feedback submits an empty description and business operator can process an
|
|
|
1009
1091
|
throw new Error('The exported large log attachment is missing')
|
|
1010
1092
|
expect(bundle.files[largeLog.path]).toEqual(new Uint8Array(feedbackLog))
|
|
1011
1093
|
expect(bundle.manifest.files.some(file => file.name === 'discarded.txt')).toBe(false)
|
|
1012
|
-
await testInfo.attach('feedback-export-masked', {
|
|
1094
|
+
await testInfo.attach('feedback-export-masked', {
|
|
1095
|
+
body: Buffer.from(bundle.files[masked.path]!),
|
|
1096
|
+
contentType: 'image/png',
|
|
1097
|
+
})
|
|
1013
1098
|
})
|
|
1014
1099
|
{{/if}}
|
|
1015
1100
|
{{/if}}
|
|
@@ -20,8 +20,13 @@ async function login(page: Page, username = 'admin', initialPassword = process.e
|
|
|
20
20
|
// 每例使用独立初始账号,真实完成服务端限制下的首次改密后才能建立普通会话。
|
|
21
21
|
await expect(page.locator('#old-password')).toBeVisible()
|
|
22
22
|
await expect(page.getByTestId('logout')).toHaveCount(0)
|
|
23
|
-
await page.screenshot({
|
|
24
|
-
|
|
23
|
+
await page.screenshot({
|
|
24
|
+
path: test.info().outputPath('initial-password-renewal.png'),
|
|
25
|
+
fullPage: true,
|
|
26
|
+
})
|
|
27
|
+
const form = page.locator('form').filter({
|
|
28
|
+
has: page.locator('#old-password'),
|
|
29
|
+
})
|
|
25
30
|
const error = page.locator('.hai-toast-item[data-variant=error]')
|
|
26
31
|
await dismissToasts(page)
|
|
27
32
|
// 同密码仍由服务端拒绝,显示安全原因与关联编号;本地确认不一致则直接提示字段。
|
|
@@ -34,7 +39,10 @@ async function login(page: Page, username = 'admin', initialPassword = process.e
|
|
|
34
39
|
await error.getByRole('button', { name: /^(查看排查信息|View diagnostic details|調査情報を表示)$/ }).click()
|
|
35
40
|
await expect(error.locator('code')).toHaveText(/^[\w.:-]{1,128}$/)
|
|
36
41
|
await expect(page.getByTestId('logout')).toHaveCount(0)
|
|
37
|
-
await page.screenshot({
|
|
42
|
+
await page.screenshot({
|
|
43
|
+
path: test.info().outputPath('initial-password-same.png'),
|
|
44
|
+
fullPage: true,
|
|
45
|
+
})
|
|
38
46
|
await expect(form.locator('.alert-error')).toHaveCount(0)
|
|
39
47
|
await dismissToasts(page)
|
|
40
48
|
const password = `${initialPassword}-Renewed!`
|
|
@@ -100,13 +108,19 @@ test('server idle expiry opens normal login in place and resumes one rejected sa
|
|
|
100
108
|
const dialog = page.getByTestId('session-recovery-dialog')
|
|
101
109
|
await expect(dialog).toBeVisible()
|
|
102
110
|
await expect(page.getByTestId('business-notice-input').locator('textarea')).toHaveValue(draft)
|
|
103
|
-
await page.screenshot({
|
|
111
|
+
await page.screenshot({
|
|
112
|
+
path: test.info().outputPath('idle-session-recovery.png'),
|
|
113
|
+
fullPage: true,
|
|
114
|
+
})
|
|
104
115
|
const form = dialog.locator('form.login-form')
|
|
105
116
|
await form.locator('input[type=password]').fill('Incorrect-test-password!')
|
|
106
117
|
await form.locator('button[type=submit]').click()
|
|
107
118
|
await expect(page.locator('.hai-toast-item[data-variant=error]')).toBeVisible()
|
|
108
119
|
await expect(page.locator('.hai-toast-item[data-variant=error]')).toBeInViewport({ ratio: 1 })
|
|
109
|
-
await page.screenshot({
|
|
120
|
+
await page.screenshot({
|
|
121
|
+
path: test.info().outputPath('idle-session-recovery-error-toast.png'),
|
|
122
|
+
fullPage: true,
|
|
123
|
+
})
|
|
110
124
|
expect(statuses).toHaveLength(1)
|
|
111
125
|
await form.locator('input[type=password]').fill(password)
|
|
112
126
|
await form.locator('button[type=submit]').click()
|
|
@@ -3,7 +3,9 @@ import { haiQualityConfig } from './eslint-rules.js'
|
|
|
3
3
|
|
|
4
4
|
export default antfu({
|
|
5
5
|
// unused-imports 使用 TS 规则,JS 同样使用 TS parser 以保持 scope 契约一致。
|
|
6
|
-
typescript: {
|
|
6
|
+
typescript: {
|
|
7
|
+
files: [GLOB_SRC],
|
|
8
|
+
},
|
|
7
9
|
formatters: true,
|
|
8
10
|
ignores: [
|
|
9
11
|
'**/build/**',
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
ARG HAI_NODE_IMAGE=node:24.22.1-bookworm-slim
|
|
2
2
|
FROM ${HAI_NODE_IMAGE} AS build
|
|
3
|
-
ARG SQLITE_PREBUILD_MIRROR=
|
|
4
|
-
ENV npm_config_better_sqlite3_binary_host_mirror=$SQLITE_PREBUILD_MIRROR
|
|
5
3
|
|
|
6
4
|
ENV PNPM_HOME=/pnpm
|
|
7
5
|
ENV PATH=$PNPM_HOME:$PATH
|
|
@@ -26,8 +24,9 @@ ARG HAI_BUILD_ID=
|
|
|
26
24
|
ARG HAI_BUILD_COMMIT=unknown
|
|
27
25
|
RUN HAI_BUILD_ID="$HAI_BUILD_ID" HAI_BUILD_COMMIT="$HAI_BUILD_COMMIT" pnpm --filter {{fullstack.servPackageName}}... --if-present build
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
RUN
|
|
27
|
+
# SQLite 13 的原生二进制随 npm 包交付,在构建时验证实际加载。
|
|
28
|
+
RUN if [ -d node_modules/better-sqlite3 ]; then node -e "const Database=require('better-sqlite3');const db=new Database(':memory:');db.prepare('SELECT 1').get();db.close()"; fi
|
|
29
|
+
RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm pm deploy --filter {{fullstack.servPackageName}} --prod /prod/service
|
|
31
30
|
RUN rm -f /prod/service/README* && find /prod/service/dist -type f \( -name '*.map' -o -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' \) -delete
|
|
32
31
|
|
|
33
32
|
FROM ${HAI_NODE_IMAGE} AS service
|
|
@@ -2,8 +2,6 @@ ARG HAI_NODE_IMAGE=node:24.22.1-bookworm-slim
|
|
|
2
2
|
ARG HAI_NGINX_IMAGE=nginxinc/nginx-unprivileged:1.29.8-alpine
|
|
3
3
|
{{#if fullstack.frontends.web}}
|
|
4
4
|
FROM ${HAI_NODE_IMAGE} AS build
|
|
5
|
-
ARG SQLITE_PREBUILD_MIRROR=
|
|
6
|
-
ENV npm_config_better_sqlite3_binary_host_mirror=$SQLITE_PREBUILD_MIRROR
|
|
7
5
|
|
|
8
6
|
ENV PNPM_HOME=/pnpm
|
|
9
7
|
ENV PATH=$PNPM_HOME:$PATH
|
package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-contract.ts.hbs
CHANGED
|
@@ -22,54 +22,145 @@ import {
|
|
|
22
22
|
/** mini 管理示例的业务路径;四类职责与权限由应用配置,可按项目需求调整。 */
|
|
23
23
|
export const adminContract = {
|
|
24
24
|
permissions: {
|
|
25
|
-
list: apiContract.route({
|
|
26
|
-
|
|
25
|
+
list: apiContract.route({
|
|
26
|
+
method: 'POST',
|
|
27
|
+
path: '/admin/permissions/list',
|
|
28
|
+
tags: ['admin'],
|
|
29
|
+
}).input(z.object({
|
|
30
|
+
page: z.number().int().min(1).default(1),
|
|
31
|
+
pageSize: z.number().int().min(1).max(100).default(20),
|
|
32
|
+
})).output(apiContract.haiResultSchema(z.object({
|
|
33
|
+
items: z.array(AdminPermissionPolicySchema),
|
|
34
|
+
total: z.number(),
|
|
35
|
+
}))),
|
|
36
|
+
update: apiContract.route({
|
|
37
|
+
method: 'POST',
|
|
38
|
+
path: '/admin/permissions/update',
|
|
39
|
+
tags: ['admin'],
|
|
40
|
+
}).input(AdminPermissionPolicyInputSchema).output(apiContract.haiResultSchema(z.object({
|
|
41
|
+
saved: z.literal(true),
|
|
42
|
+
}))),
|
|
27
43
|
},
|
|
28
44
|
configuration: {
|
|
29
|
-
get: apiContract.route({
|
|
30
|
-
|
|
45
|
+
get: apiContract.route({
|
|
46
|
+
method: 'POST',
|
|
47
|
+
path: '/admin/configuration/get',
|
|
48
|
+
tags: ['admin'],
|
|
49
|
+
}).output(apiContract.haiResultSchema(BusinessSettingsSchema)),
|
|
50
|
+
update: apiContract.route({
|
|
51
|
+
method: 'POST',
|
|
52
|
+
path: '/admin/configuration/update',
|
|
53
|
+
tags: ['admin'],
|
|
54
|
+
}).input(BusinessSettingsSchema).output(apiContract.haiResultSchema(BusinessSettingsSchema)),
|
|
31
55
|
},
|
|
32
56
|
users: {
|
|
33
|
-
resetPassword: apiContract.route({
|
|
57
|
+
resetPassword: apiContract.route({
|
|
58
|
+
method: 'POST',
|
|
59
|
+
path: '/admin/users/reset-password',
|
|
60
|
+
tags: ['admin'],
|
|
61
|
+
})
|
|
34
62
|
.input(IamAdminResetPasswordInputSchema.strict())
|
|
35
|
-
.output(apiContract.haiResultSchema(z.object({
|
|
36
|
-
|
|
37
|
-
|
|
63
|
+
.output(apiContract.haiResultSchema(z.object({
|
|
64
|
+
reset: z.literal(true),
|
|
65
|
+
}))),
|
|
66
|
+
invite: apiContract.route({
|
|
67
|
+
method: 'POST',
|
|
68
|
+
path: '/admin/users/invite',
|
|
69
|
+
tags: ['admin'],
|
|
70
|
+
}).input(AdminInviteUserSchema).output(apiContract.haiResultSchema(AdminUserSchema)),
|
|
71
|
+
list: apiContract.route({
|
|
72
|
+
method: 'POST',
|
|
73
|
+
path: '/admin/users/list',
|
|
74
|
+
tags: ['admin'],
|
|
75
|
+
})
|
|
38
76
|
.input(AdminUsersInputSchema)
|
|
39
77
|
.output(apiContract.haiResultSchema(AdminUsersSchema)),
|
|
40
|
-
create: apiContract.route({
|
|
78
|
+
create: apiContract.route({
|
|
79
|
+
method: 'POST',
|
|
80
|
+
path: '/admin/users/create',
|
|
81
|
+
tags: ['admin'],
|
|
82
|
+
})
|
|
41
83
|
.input(AdminCreateUserSchema)
|
|
42
84
|
.output(apiContract.haiResultSchema(AdminUserSchema)),
|
|
43
|
-
update: apiContract.route({
|
|
85
|
+
update: apiContract.route({
|
|
86
|
+
method: 'POST',
|
|
87
|
+
path: '/admin/users/update',
|
|
88
|
+
tags: ['admin'],
|
|
89
|
+
})
|
|
44
90
|
.input(AdminUpdateUserSchema)
|
|
45
91
|
.output(apiContract.haiResultSchema(AdminUserSchema)),
|
|
46
|
-
delete: apiContract.route({
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
92
|
+
delete: apiContract.route({
|
|
93
|
+
method: 'POST',
|
|
94
|
+
path: '/admin/users/delete',
|
|
95
|
+
tags: ['admin'],
|
|
96
|
+
})
|
|
97
|
+
.input(z.object({
|
|
98
|
+
id: z.string().min(1),
|
|
99
|
+
}).strict())
|
|
100
|
+
.output(apiContract.haiResultSchema(z.object({
|
|
101
|
+
deleted: z.literal(true),
|
|
102
|
+
}))),
|
|
103
|
+
avatar: apiContract.route({
|
|
104
|
+
method: 'POST',
|
|
105
|
+
path: '/admin/users/avatar',
|
|
106
|
+
tags: ['admin'],
|
|
107
|
+
})
|
|
108
|
+
.input(AvatarUploadSchema.extend({
|
|
109
|
+
id: z.string().min(1),
|
|
110
|
+
}))
|
|
51
111
|
.output(apiContract.haiResultSchema(AvatarOutputSchema)),
|
|
52
112
|
},
|
|
53
113
|
roles: {
|
|
54
|
-
list: apiContract.route({
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
114
|
+
list: apiContract.route({
|
|
115
|
+
method: 'POST',
|
|
116
|
+
path: '/admin/roles/list',
|
|
117
|
+
tags: ['admin'],
|
|
118
|
+
})
|
|
119
|
+
.output(apiContract.haiResultSchema(z.object({
|
|
120
|
+
items: z.array(AdminRoleSchema),
|
|
121
|
+
}))),
|
|
122
|
+
update: apiContract.route({
|
|
123
|
+
method: 'POST',
|
|
124
|
+
path: '/admin/roles/update',
|
|
125
|
+
tags: ['admin'],
|
|
126
|
+
})
|
|
127
|
+
.input(z.object({
|
|
128
|
+
id: z.string().min(1),
|
|
129
|
+
description: z.string().max(300),
|
|
130
|
+
}).strict())
|
|
58
131
|
.output(apiContract.haiResultSchema(AdminRoleSchema)),
|
|
59
132
|
},
|
|
60
133
|
audit: {
|
|
61
|
-
backup: apiContract.route({
|
|
62
|
-
|
|
134
|
+
backup: apiContract.route({
|
|
135
|
+
method: 'POST',
|
|
136
|
+
path: '/admin/audit/backup',
|
|
137
|
+
tags: ['admin'],
|
|
138
|
+
}).input(z.object({
|
|
139
|
+
id: z.uuid(),
|
|
140
|
+
}).strict()).output(apiContract.haiResultSchema(AuditBackupReceiptSchema)),
|
|
141
|
+
list: apiContract.route({
|
|
142
|
+
method: 'POST',
|
|
143
|
+
path: '/admin/audit/list',
|
|
144
|
+
tags: ['admin'],
|
|
145
|
+
})
|
|
63
146
|
.input(AdminAuditInputSchema)
|
|
64
147
|
.output(apiContract.haiResultSchema(AdminAuditSchema)),
|
|
65
148
|
},
|
|
66
149
|
}
|
|
67
150
|
|
|
68
151
|
export const profileContract = {
|
|
69
|
-
update: apiContract.route({
|
|
152
|
+
update: apiContract.route({
|
|
153
|
+
method: 'POST',
|
|
154
|
+
path: '/profile/update',
|
|
155
|
+
tags: ['profile'],
|
|
156
|
+
})
|
|
70
157
|
.input(ProfileUpdateSchema)
|
|
71
158
|
.output(apiContract.haiResultSchema(AdminUserSchema)),
|
|
72
|
-
avatar: apiContract.route({
|
|
159
|
+
avatar: apiContract.route({
|
|
160
|
+
method: 'POST',
|
|
161
|
+
path: '/profile/avatar',
|
|
162
|
+
tags: ['profile'],
|
|
163
|
+
})
|
|
73
164
|
.input(AvatarUploadSchema)
|
|
74
165
|
.output(apiContract.haiResultSchema(AvatarOutputSchema)),
|
|
75
166
|
}
|