@h-ai/cli 0.1.0-alpha9 → 1.0.0-beta.2
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 +229 -0
- package/README.ja.md +229 -0
- package/README.md +175 -113
- package/dist/chunk-AKV2CQZV.js +1497 -0
- package/dist/chunk-AKV2CQZV.js.map +1 -0
- package/dist/chunk-EF7WOFWZ.js +7557 -0
- package/dist/chunk-EF7WOFWZ.js.map +1 -0
- package/dist/chunk-MUHRBJSW.js +29272 -0
- package/dist/chunk-MUHRBJSW.js.map +1 -0
- package/dist/chunk-ZSDEVXZ7.js +38 -0
- package/dist/{chunk-2H7UOFLK.js.map → chunk-ZSDEVXZ7.js.map} +1 -1
- package/dist/cli-config-templates-HHFHNXBN.js +5 -0
- package/dist/cli-config-templates-HHFHNXBN.js.map +1 -0
- package/dist/cli-skill-templates-PN4FLGBC.js +5 -0
- package/dist/cli-skill-templates-PN4FLGBC.js.map +1 -0
- package/dist/index.js +4444 -554
- package/dist/index.js.map +1 -1
- package/dist/metafile-esm.json +1 -0
- package/dist/src-TEBHR5I5.js +5 -0
- package/dist/src-TEBHR5I5.js.map +1 -0
- package/package.json +35 -15
- package/templates/README.md +56 -114
- package/templates/apps/admin/messages/en-US.json +7 -0
- package/templates/apps/admin/messages/ja-JP.json +40 -0
- package/templates/apps/admin/messages/zh-CN.json +7 -0
- package/templates/apps/admin/src/routes/+layout.svelte.hbs +65 -8
- package/templates/apps/admin/src/routes/+page.svelte.hbs +35 -14
- package/templates/apps/admin/src/routes/admin/+layout.svelte.hbs +54 -22
- package/templates/apps/admin/src/routes/admin/+page.svelte.hbs +33 -24
- package/templates/apps/admin/src/routes/admin/settings/+page.svelte.hbs +16 -13
- package/templates/apps/api/.dockerignore.hbs +25 -0
- package/templates/apps/api/.gitignore.hbs +22 -0
- package/templates/apps/api/.npmrc.hbs +1 -0
- package/templates/apps/api/apps/{{projectName}}-contract/README.md.hbs +39 -0
- package/templates/apps/api/apps/{{projectName}}-contract/package.json.hbs +41 -0
- package/templates/apps/api/apps/{{projectName}}-contract/src/app-contract.ts.hbs +27 -0
- package/templates/apps/api/apps/{{projectName}}-contract/src/app-schemas.ts.hbs +28 -0
- package/templates/apps/api/apps/{{projectName}}-contract/src/index.ts.hbs +5 -0
- package/templates/apps/api/apps/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +16 -0
- package/templates/apps/api/apps/{{projectName}}-contract/tests/contract.test.ts.hbs +18 -0
- package/templates/apps/api/apps/{{projectName}}-contract/tsconfig.json.hbs +12 -0
- package/templates/apps/api/apps/{{projectName}}-contract/tsup.config.ts.hbs +11 -0
- package/templates/apps/api/apps/{{projectName}}-contract/vitest.config.ts.hbs +9 -0
- package/templates/apps/api/apps/{{projectName}}-serv/README.md.hbs +60 -0
- package/templates/apps/api/apps/{{projectName}}-serv/config/_core.yml.hbs +17 -0
- package/templates/apps/api/apps/{{projectName}}-serv/config/_serv.yml.hbs +54 -0
- package/templates/apps/api/apps/{{projectName}}-serv/data/.gitkeep.hbs +1 -0
- package/templates/apps/api/apps/{{projectName}}-serv/package.json.hbs +66 -0
- package/templates/apps/api/apps/{{projectName}}-serv/src/app.ts.hbs +119 -0
- package/templates/apps/api/apps/{{projectName}}-serv/src/build-metadata.ts.hbs +7 -0
- package/templates/apps/api/apps/{{projectName}}-serv/src/index.ts.hbs +41 -0
- package/templates/apps/api/apps/{{projectName}}-serv/src/lib/server/init.ts.hbs +165 -0
- package/templates/apps/api/apps/{{projectName}}-serv/src/server/procedures/app-procedures.ts.hbs +57 -0
- package/templates/apps/api/apps/{{projectName}}-serv/src/server/procedures/index.ts.hbs +41 -0
- package/templates/apps/api/apps/{{projectName}}-serv/tests/service.test.ts.hbs +176 -0
- package/templates/apps/api/apps/{{projectName}}-serv/tsconfig.json.hbs +16 -0
- package/templates/apps/api/apps/{{projectName}}-serv/tsup.config.ts.hbs +20 -0
- package/templates/apps/api/apps/{{projectName}}-serv/vitest.config.ts.hbs +12 -0
- package/templates/apps/api/e2e/api-serv.spec.ts.hbs +113 -0
- package/templates/apps/api/e2e/capture-evidence.ts.hbs +23 -0
- package/templates/apps/api/eslint.config.js.hbs +30 -0
- package/templates/apps/api/hai.json.hbs +4 -0
- package/templates/apps/api/images/serv.Dockerfile.hbs +37 -0
- package/templates/apps/api/package.json.hbs +53 -0
- package/templates/apps/api/playwright.config.ts.hbs +67 -0
- package/templates/apps/api/pnpm-workspace.yaml.hbs +40 -0
- package/templates/apps/api/tsconfig.base.json +14 -0
- package/templates/apps/fullstack/.dockerignore.hbs +25 -0
- package/templates/apps/fullstack/.gitignore.hbs +38 -0
- package/templates/apps/fullstack/.npmrc.hbs +1 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/.env.example.hbs +10 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +67 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/capacitor.config.ts.hbs +20 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/eslint.config.js.hbs +20 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/index.html.hbs +22 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/messages/en-US.json.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/messages/ja-JP.json.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/messages/zh-CN.json.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/package.json.hbs +86 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/project.inlang/settings.json.hbs +17 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/public/runtime-config.js.hbs +4 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/src/App.svelte.hbs +59 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/src/app.css.hbs +20 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/src/app.d.ts.hbs +8 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/src/build-metadata.ts.hbs +9 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/src/lib/capacitor.ts.hbs +19 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/src/main.ts.hbs +21 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/svelte.config.js.hbs +16 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/tsconfig.json.hbs +23 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +74 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/.env.example.hbs +6 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +64 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/eslint.config.js.hbs +20 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/index.html.hbs +20 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/messages/en-US.json.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/messages/ja-JP.json.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/messages/zh-CN.json.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/package.json.hbs +66 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/project.inlang/settings.json.hbs +17 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/App.svelte.hbs +25 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/app.css.hbs +18 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/app.d.ts.hbs +8 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/build-metadata.ts.hbs +9 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/main.ts.hbs +20 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/Cargo.toml.hbs +33 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/build.rs.hbs +6 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/capabilities/default.json.hbs +16 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/icons/128x128.png +0 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/icons/128x128@2x.png +0 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/icons/32x32.png +0 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/icons/icon.icns +0 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/icons/icon.ico +0 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/src/lib.rs.hbs +91 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/src/main.rs.hbs +8 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/tauri.conf.json.hbs +46 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/svelte.config.js.hbs +14 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/tsconfig.json.hbs +22 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +89 -0
- package/templates/apps/fullstack/apps/{{projectName}}-miniapp/README.md.hbs +25 -0
- package/templates/apps/fullstack/apps/{{projectName}}-miniapp/messages/en-US.json.hbs +12 -0
- package/templates/apps/fullstack/apps/{{projectName}}-miniapp/messages/ja-JP.json.hbs +12 -0
- package/templates/apps/fullstack/apps/{{projectName}}-miniapp/messages/zh-CN.json.hbs +12 -0
- package/templates/apps/fullstack/apps/{{projectName}}-miniapp/project.inlang/settings.json.hbs +17 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +54 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/eslint.config.js.hbs +20 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/index.html.hbs +21 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/messages/en-US.json.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/messages/ja-JP.json.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/messages/zh-CN.json.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/package.json.hbs +53 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/project.inlang/settings.json.hbs +17 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/public/runtime-config.js.hbs +1 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/src/App.svelte.hbs +18 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/src/app.css.hbs +18 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/src/app.d.ts.hbs +8 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/src/build-metadata.ts.hbs +9 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/src/main.ts.hbs +14 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/svelte.config.js.hbs +16 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/tsconfig.json.hbs +23 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +76 -0
- package/templates/apps/fullstack/docs/security-configuration.md.hbs +349 -0
- package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +988 -0
- package/templates/apps/fullstack/e2e/session-close.spec.ts.hbs +119 -0
- package/templates/apps/fullstack/eslint.config.js.hbs +41 -0
- package/templates/apps/fullstack/hai.json.hbs +11 -0
- package/templates/apps/fullstack/images/serv.Dockerfile.hbs +38 -0
- package/templates/apps/fullstack/images/web/19-runtime-config.envsh.hbs +5 -0
- package/templates/apps/fullstack/images/web/nginx.conf.hbs +24 -0
- package/templates/apps/fullstack/images/web/runtime-config.js.template.hbs +3 -0
- package/templates/apps/fullstack/images/web.Dockerfile.hbs +37 -0
- package/templates/apps/fullstack/package.json.hbs +61 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/README.md.hbs +40 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/package.json.hbs +30 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-contract.ts.hbs +78 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-schemas.ts.hbs +134 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/app-contract.ts.hbs +20 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/app-schemas.ts.hbs +35 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/index.ts.hbs +7 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +23 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/tests/contract.test.ts.hbs +31 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/tsconfig.json.hbs +9 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/tsup.config.ts.hbs +9 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +84 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_ai.yml.hbs +1 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_audit.yml.hbs +1 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_cache.yml.hbs +1 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_core.yml.hbs +22 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_db.yml.hbs +1 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_feedback.yml.hbs +1 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_iam.yml.hbs +1 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_payment.yml.hbs +1 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_reach.yml.hbs +60 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_scheduler.yml.hbs +1 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +64 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_storage.yml.hbs +1 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_vecdb.yml.hbs +1 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/data/.gitkeep.hbs +1 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +62 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/app-server.ts.hbs +115 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/build-metadata.ts.hbs +7 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/index.ts.hbs +40 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/admin-service.ts.hbs +294 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/audit-jobs.ts.hbs +28 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/business-settings.ts.hbs +50 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/messages.ts.hbs +50 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/admin-procedures.ts.hbs +106 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/app-procedures.ts.hbs +49 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/index.ts.hbs +34 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/runtime.ts.hbs +159 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/security-roles.ts.hbs +73 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/app-server.test.ts.hbs +505 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/runtime.test.ts.hbs +72 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/security.test.ts.hbs +183 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/tsconfig.json.hbs +10 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/tsup.config.ts.hbs +18 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/vitest.config.ts.hbs +12 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/.gitignore.hbs +10 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +97 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/en-US.json.hbs +164 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/ja-JP.json.hbs +164 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/zh-CN.json.hbs +164 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/package.json.hbs +55 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/project.inlang/settings.json.hbs +11 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/index.ts.hbs +7 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/api-client.ts.hbs +94 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/feedback-client.ts.hbs +36 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/sensitive-actions.svelte.ts.hbs +16 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/session-recovery.svelte.ts.hbs +4 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AdminWorkspace.svelte.hbs +122 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +90 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/FeedbackEntry.svelte.hbs +86 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/audit.svelte.hbs +94 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/configuration.svelte.hbs +25 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/feedback.svelte.hbs +157 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/roles.svelte.hbs +77 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/settings.svelte.hbs +44 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/users.svelte.hbs +216 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/security/role-label.ts.hbs +12 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/guide-store.ts.hbs +55 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/management-store.svelte.ts.hbs +614 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/theme-store.svelte.ts.hbs +23 -0
- 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 +150 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/management-store.test.ts.hbs +253 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/tsconfig.json.hbs +17 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/vitest.config.ts.hbs +7 -0
- package/templates/apps/fullstack/playwright.config.ts.hbs +114 -0
- package/templates/apps/fullstack/playwright.session-close.config.ts.hbs +18 -0
- package/templates/apps/fullstack/pnpm-workspace.yaml.hbs +38 -0
- package/templates/apps/fullstack/tsconfig.base.json +14 -0
- package/templates/apps/h5/messages/en-US.json +3 -0
- package/templates/apps/h5/messages/ja-JP.json +40 -0
- package/templates/apps/h5/messages/zh-CN.json +3 -0
- package/templates/apps/h5/src/routes/+layout.svelte.hbs +85 -30
- package/templates/apps/h5/src/routes/+page.svelte.hbs +20 -8
- package/templates/apps/h5/src/routes/discover/+page.svelte.hbs +17 -6
- package/templates/apps/h5/src/routes/profile/+page.svelte.hbs +17 -6
- package/templates/apps/mobile-app/.env.hbs +3 -0
- package/templates/apps/{android-app → mobile-app}/capacitor.config.ts.hbs +2 -2
- package/templates/apps/mobile-app/index.html.hbs +14 -0
- package/templates/apps/mobile-app/messages/en-US.json +36 -0
- package/templates/apps/mobile-app/messages/ja-JP.json +36 -0
- package/templates/apps/mobile-app/messages/zh-CN.json +36 -0
- package/templates/apps/mobile-app/public/runtime-config.js +1 -0
- package/templates/apps/mobile-app/src/App.svelte.hbs +153 -0
- package/templates/apps/mobile-app/src/lib/api.ts.hbs +213 -0
- package/templates/apps/mobile-app/src/lib/app-config.ts.hbs +14 -0
- package/templates/apps/{android-app → mobile-app}/src/lib/capacitor.ts +6 -7
- package/templates/apps/mobile-app/src/lib/pages/home.svelte.hbs +58 -0
- package/templates/apps/mobile-app/src/lib/pages/profile.svelte.hbs +13 -0
- package/templates/apps/mobile-app/src/lib/pages/tasks.svelte.hbs +16 -0
- package/templates/apps/mobile-app/src/main.ts.hbs +11 -0
- package/templates/apps/website/messages/en-US.json +4 -2
- package/templates/apps/website/messages/ja-JP.json +34 -0
- package/templates/apps/website/messages/zh-CN.json +4 -2
- package/templates/apps/website/scripts/static-website.mjs.hbs +67 -0
- package/templates/apps/website/src/routes/+layout.svelte.hbs +77 -26
- package/templates/apps/website/src/routes/+layout.ts +3 -0
- package/templates/apps/website/src/routes/+page.svelte.hbs +31 -12
- package/templates/apps/website/src/routes/about/+page.svelte.hbs +11 -6
- package/templates/apps/website/src/routes/api/health/+server.ts +3 -0
- package/templates/apps/website/src/routes/contact/+page.svelte.hbs +44 -34
- package/templates/apps/website/src/routes/services/+page.svelte.hbs +27 -40
- package/templates/base/.dockerignore.hbs +27 -0
- package/templates/base/.gitignore.hbs +41 -0
- package/templates/base/.npmrc.hbs +1 -0
- package/templates/base/e2e/app.spec.ts.hbs +213 -0
- package/templates/base/e2e/helpers.ts +1 -1
- package/templates/base/eslint-rules.js +45 -0
- package/templates/base/eslint.config.js.hbs +34 -0
- package/templates/base/hai.json.hbs +4 -0
- package/templates/base/images/web/nginx.conf.hbs +24 -0
- package/templates/base/images/web.Dockerfile.hbs +72 -0
- package/templates/base/package.json.hbs +125 -49
- package/templates/base/playwright.config.ts +44 -6
- package/templates/base/pnpm-workspace.yaml.hbs +40 -0
- package/templates/base/src/app.css.hbs +40 -0
- package/templates/base/src/app.d.ts.hbs +14 -1
- package/templates/base/src/app.html.hbs +13 -0
- package/templates/base/src/hooks.server.ts.hbs +115 -43
- package/templates/base/src/lib/build-metadata.ts.hbs +7 -0
- package/templates/base/src/lib/server/init.ts.hbs +125 -152
- package/templates/base/src/lib/server/kit-config.ts.hbs +40 -0
- package/templates/base/src/routes/+layout.server.ts.hbs +35 -0
- package/templates/base/static/favicon.png +0 -0
- package/templates/base/svelte.config.js.hbs +84 -5
- package/templates/base/tests/scaffold.test.ts.hbs +102 -0
- package/templates/base/tsconfig.json.hbs +50 -0
- package/templates/base/vite.config.ts.hbs +117 -12
- package/templates/base/vitest.config.ts.hbs +19 -0
- package/templates/features/ai/routes/api/chat/+server.ts +6 -7
- package/templates/features/iam/routes-admin/(auth)/+layout.svelte.hbs +3 -17
- 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 +9 -96
- package/templates/features/iam/routes-admin/(auth)/auth/register/+page.svelte.hbs +9 -122
- 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 +9 -96
- package/templates/features/iam/routes-h5/auth/register/+page.svelte.hbs +9 -112
- package/templates/features/iam/routes-h5/auth/reset-password/+page.svelte.hbs +10 -0
- package/templates/features/iam/routes-shared/api/auth/captcha/+server.ts.hbs +4 -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 -29
- 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/logout/+server.ts +13 -6
- package/templates/features/iam/routes-shared/api/auth/otp/send/+server.ts +4 -0
- package/templates/features/iam/routes-shared/api/auth/password/renew/+server.ts +5 -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/reauthenticate/+server.ts +5 -0
- package/templates/features/iam/routes-shared/api/auth/register/+server.ts +1 -23
- package/templates/features/iam/routes-shared/api/auth/session/close/+server.ts.hbs +4 -0
- 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/features/payment/routes/api/v1/payment/create/+server.ts +25 -23
- package/templates/features/payment/routes/api/v1/payment/notify/[provider]/+server.ts +5 -2
- package/templates/features/storage/routes/api/upload/+server.ts +10 -8
- package/templates/i18n/project.inlang/settings.json +2 -2
- package/templates/shared/.gitattributes +11 -0
- package/templates/shared/build-metadata.ts +42 -0
- package/templates/shared/container-runtime-config.sh +30 -0
- package/templates/shared/e2e-env.ts +98 -0
- package/templates/shared/hash-router.svelte.ts +83 -0
- package/templates/shared/public-base-path.mjs +19 -0
- package/templates/shared/run-e2e.mjs +104 -0
- package/templates/shared/vite-runtime-config.ts +56 -0
- package/templates/skills/AGENTS.md +75 -16
- package/templates/skills/CLAUDE.md +0 -11
- package/templates/skills/bridges/admin/AGENTS.md +89 -0
- package/templates/skills/bridges/api/AGENTS.md +83 -0
- package/templates/skills/bridges/fullstack/AGENTS.md.hbs +102 -0
- package/templates/skills/bridges/h5/AGENTS.md +88 -0
- package/templates/skills/bridges/mobile-app/AGENTS.md +100 -0
- package/templates/skills/bridges/website/AGENTS.md +88 -0
- package/templates/skills/copilot-instructions.md +3 -59
- package/templates/skills/hai-ai/SKILL.md +44 -715
- package/templates/skills/hai-ai/guide.md +457 -0
- package/templates/skills/hai-ai/reference.md +292 -0
- package/templates/skills/hai-api-client/SKILL.md +26 -339
- package/templates/skills/hai-api-client/reference.md +180 -0
- package/templates/skills/hai-api-contract/SKILL.md +36 -0
- package/templates/skills/hai-api-contract/reference.md +280 -0
- package/templates/skills/hai-app-create/SKILL.md +38 -351
- package/templates/skills/hai-app-create/collaboration.md +52 -0
- package/templates/skills/hai-app-create/development.md +243 -0
- package/templates/skills/hai-app-create/reference.md +376 -0
- package/templates/skills/hai-app-review/SKILL.md +57 -126
- package/templates/skills/hai-app-review/differential.md +34 -0
- package/templates/skills/hai-app-tests/SKILL.md +34 -454
- package/templates/skills/hai-app-tests/reference.md +462 -0
- package/templates/skills/hai-app-tests/validation.md +42 -0
- package/templates/skills/hai-audit/SKILL.md +37 -122
- package/templates/skills/hai-audit/reference.md +184 -0
- package/templates/skills/hai-build/SKILL.md +62 -315
- package/templates/skills/hai-build/paths.md +30 -0
- package/templates/skills/hai-build/reference.md +353 -0
- package/templates/skills/hai-cache/SKILL.md +26 -175
- package/templates/skills/hai-cache/reference.md +229 -0
- package/templates/skills/hai-capacitor/SKILL.md +24 -364
- package/templates/skills/hai-capacitor/reference.md +358 -0
- package/templates/skills/hai-change-plan/SKILL.md +46 -0
- package/templates/skills/hai-ci/SKILL.md +114 -0
- package/templates/skills/hai-core/SKILL.md +31 -469
- package/templates/skills/hai-core/reference.md +557 -0
- package/templates/skills/hai-core/request-access.md +65 -0
- package/templates/skills/hai-crypto/SKILL.md +29 -180
- package/templates/skills/hai-crypto/reference.md +317 -0
- package/templates/skills/hai-datapipe/SKILL.md +24 -207
- package/templates/skills/hai-datapipe/reference.md +204 -0
- package/templates/skills/hai-deploy/SKILL.md +53 -178
- package/templates/skills/hai-deploy/credentials.md +19 -0
- package/templates/skills/hai-deploy/middleware.md +129 -0
- package/templates/skills/hai-feedback/SKILL.md +85 -0
- package/templates/skills/hai-framework-sync/SKILL.md +105 -0
- package/templates/skills/hai-framework-sync/sync.md +49 -0
- package/templates/skills/hai-iam/SKILL.md +53 -325
- package/templates/skills/hai-iam/reference.md +535 -0
- package/templates/skills/hai-kit/SKILL.md +33 -329
- package/templates/skills/hai-kit/reference.md +202 -0
- package/templates/skills/hai-payment/SKILL.md +28 -262
- package/templates/skills/hai-payment/reference.md +295 -0
- package/templates/skills/hai-pr-review/SKILL.md +41 -0
- package/templates/skills/hai-reach/SKILL.md +28 -300
- package/templates/skills/hai-reach/reference.md +363 -0
- package/templates/skills/hai-reldb/SKILL.md +28 -449
- package/templates/skills/hai-reldb/reference.md +500 -0
- package/templates/skills/hai-scheduler/SKILL.md +26 -174
- package/templates/skills/hai-scheduler/reference.md +216 -0
- package/templates/skills/hai-serv/SKILL.md +60 -0
- package/templates/skills/hai-serv/reference.md +792 -0
- package/templates/skills/hai-spec-implement/SKILL.md +50 -0
- package/templates/skills/hai-spec-implement/changes.md +67 -0
- package/templates/skills/hai-spec-implement/evaluation.md +18 -0
- package/templates/skills/hai-spec-implement/handoff.md +47 -0
- package/templates/skills/hai-spec-implement/templates.md +117 -0
- package/templates/skills/hai-spec-implement/workflow.md +56 -0
- package/templates/skills/hai-spec-review/SKILL.md +57 -0
- package/templates/skills/hai-spec-review/checklist.md +71 -0
- package/templates/skills/hai-storage/SKILL.md +31 -262
- package/templates/skills/hai-storage/reference.md +284 -0
- package/templates/skills/hai-ui/SKILL.md +62 -238
- package/templates/skills/hai-ui/guide.md +81 -0
- package/templates/skills/hai-ui/reference.md +705 -0
- package/templates/skills/hai-ui-design/SKILL.md +31 -0
- package/templates/skills/hai-ui-design/patterns.md +31 -0
- package/templates/skills/hai-ui-review/SKILL.md +32 -0
- package/templates/skills/hai-ui-review/checklist.md +29 -0
- package/templates/skills/hai-vecdb/SKILL.md +25 -267
- package/templates/skills/hai-vecdb/reference.md +325 -0
- package/templates/skills/opencode.json +0 -5
- package/dist/chunk-2H7UOFLK.js +0 -10
- package/dist/chunk-LANIDJC7.js +0 -659
- package/dist/chunk-LANIDJC7.js.map +0 -1
- package/dist/chunk-YV7WPUI6.js +0 -206
- package/dist/chunk-YV7WPUI6.js.map +0 -1
- package/dist/config-templates-S5RI2MC6.js +0 -5
- package/dist/config-templates-S5RI2MC6.js.map +0 -1
- package/dist/skill-templates-N5DU5FNX.js +0 -5
- package/dist/skill-templates-N5DU5FNX.js.map +0 -1
- package/dist/src-XJ6YXNAF.js +0 -15030
- package/dist/src-XJ6YXNAF.js.map +0 -1
- package/templates/apps/android-app/.env.hbs +0 -3
- package/templates/apps/android-app/messages/en-US.json +0 -24
- package/templates/apps/android-app/messages/zh-CN.json +0 -24
- package/templates/apps/android-app/src/lib/api.ts.hbs +0 -30
- package/templates/apps/android-app/src/routes/+layout.svelte.hbs +0 -42
- package/templates/apps/android-app/src/routes/+layout.ts +0 -7
- package/templates/apps/android-app/src/routes/+page.svelte.hbs +0 -27
- package/templates/apps/api/src/routes/+layout.svelte +0 -14
- package/templates/apps/api/src/routes/+page.svelte +0 -11
- package/templates/apps/api/src/routes/api/v1/health/+server.ts +0 -14
- package/templates/apps/api/src/routes/api/v1/items/+server.ts +0 -37
- package/templates/apps/api/src/routes/api/v1/items/[id]/+server.ts +0 -30
- package/templates/base/src/app.css +0 -2
- package/templates/base/tsconfig.json +0 -8
package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/admin-service.ts.hbs
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import type { HaiResult } from '@h-ai/core'
|
|
2
|
+
import type { Role, User } from '@h-ai/iam'
|
|
3
|
+
import type { ServConfig, ServContext } from '@h-ai/serv'
|
|
4
|
+
import type { AdminCreateUser, AdminInviteUser, AdminRole, AdminRoleCode, AdminUpdateUser, AdminUser, AvatarUpload, ProfileUpdate } from '{{fullstack.contractPackageName}}'
|
|
5
|
+
import { Buffer } from 'node:buffer'
|
|
6
|
+
import { audit } from '@h-ai/audit'
|
|
7
|
+
import { cache } from '@h-ai/cache'
|
|
8
|
+
import { core, err, HaiCommonError, ok } from '@h-ai/core'
|
|
9
|
+
import { iam } from '@h-ai/iam'
|
|
10
|
+
import { storage } from '@h-ai/storage'
|
|
11
|
+
import { SUPPORTED_LOCALES } from '{{fullstack.contractPackageName}}'
|
|
12
|
+
import { serverM } from './messages.js'
|
|
13
|
+
|
|
14
|
+
const ROLE_CODES = new Set<AdminRoleCode>(['admin', 'audit_admin', 'config_admin', 'business_operator', 'user'])
|
|
15
|
+
const SUPPORTED_THEMES = [{{#each fullstack.theme.themes}}'{{this}}'{{#unless @last}}, {{/unless}}{{/each}}] as readonly string[]
|
|
16
|
+
let adminMutationActive = false
|
|
17
|
+
|
|
18
|
+
/** 管理写操作串行化,锁内重新鉴权,防止两个管理员同时互相删除或降级。 */
|
|
19
|
+
export async function withAdminMutation<T>(context: ServContext, operation: string, work: () => Promise<HaiResult<T>>): Promise<HaiResult<T>> {
|
|
20
|
+
// mini 的管理写入仅支持单写实例;进程内保护无租期,慢请求不会在 TTL 到期后被并发穿透。
|
|
21
|
+
if (adminMutationActive)
|
|
22
|
+
return err(HaiCommonError.CONFLICT, serverM('busy', { locale: context.locale }))
|
|
23
|
+
adminMutationActive = true
|
|
24
|
+
let owner = ''
|
|
25
|
+
const key = 'hai:app:admin-mutation'
|
|
26
|
+
let acquired = false
|
|
27
|
+
let leaseLost = false
|
|
28
|
+
let renewalPending = false
|
|
29
|
+
let renewalTimer: ReturnType<typeof setInterval> | undefined
|
|
30
|
+
try {
|
|
31
|
+
const locked = await cache.lock.acquire(key, { ttl: 300 })
|
|
32
|
+
if (!locked.success)
|
|
33
|
+
return locked
|
|
34
|
+
if (!locked.data)
|
|
35
|
+
return err(HaiCommonError.CONFLICT, serverM('busy', { locale: context.locale }))
|
|
36
|
+
owner = locked.data
|
|
37
|
+
acquired = true
|
|
38
|
+
renewalTimer = setInterval(() => {
|
|
39
|
+
if (renewalPending || leaseLost)
|
|
40
|
+
return
|
|
41
|
+
renewalPending = true
|
|
42
|
+
void cache.lock.extend(key, 300, owner).then((renewed) => {
|
|
43
|
+
if (!renewed.success || !renewed.data) {
|
|
44
|
+
leaseLost = true
|
|
45
|
+
core.logger.error('Admin mutation lease renewal failed', { owner, error: renewed.success ? 'lease_not_owned' : renewed.error.code })
|
|
46
|
+
}
|
|
47
|
+
}).catch(() => {
|
|
48
|
+
leaseLost = true
|
|
49
|
+
core.logger.error('Admin mutation lease renewal failed', { owner, error: 'unexpected_renewal_failure' })
|
|
50
|
+
}).finally(() => {
|
|
51
|
+
renewalPending = false
|
|
52
|
+
})
|
|
53
|
+
}, 100_000)
|
|
54
|
+
renewalTimer.unref()
|
|
55
|
+
const actor = await getAdminUser(context.session?.userId ?? '')
|
|
56
|
+
if (!actor.success)
|
|
57
|
+
return actor
|
|
58
|
+
const permitted = await iam.authz.checkPermission(actor.data.id, operation.startsWith('iam.roles.') ? 'app.roles.manage' : 'app.users.manage')
|
|
59
|
+
if (!permitted.success)
|
|
60
|
+
return permitted
|
|
61
|
+
if (actor.data.accountStatus !== 'active' || (actor.data.accountType === 'temporary' && (!actor.data.accountExpiresAt || new Date(actor.data.accountExpiresAt).getTime() <= Date.now())) || !permitted.data)
|
|
62
|
+
return err(HaiCommonError.FORBIDDEN, serverM('selfProtection', { locale: context.locale }))
|
|
63
|
+
if (leaseLost)
|
|
64
|
+
return err(HaiCommonError.CONFLICT, serverM('busy', { locale: context.locale }))
|
|
65
|
+
const sensitive = await iam.auth.checkSensitiveOperation(context.accessToken ?? '', operation, context.request.headers.get('x-hai-reauthentication') ?? undefined)
|
|
66
|
+
if (!sensitive.success)
|
|
67
|
+
return sensitive
|
|
68
|
+
return await work()
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
if (renewalTimer)
|
|
72
|
+
clearInterval(renewalTimer)
|
|
73
|
+
try {
|
|
74
|
+
if (acquired) {
|
|
75
|
+
const released = await cache.lock.release(key, owner)
|
|
76
|
+
if (!released.success || !released.data)
|
|
77
|
+
core.logger.error('Failed to release admin mutation lock', { owner, error: released.success ? 'lease_not_owned' : released.error.code })
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
adminMutationActive = false
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 用户与角色读取失败直接透传,不能把角色查询失败当作普通用户。 */
|
|
87
|
+
export async function getAdminUser(id: string): Promise<HaiResult<AdminUser>> {
|
|
88
|
+
const user = await iam.user.getUser(id)
|
|
89
|
+
if (!user.success)
|
|
90
|
+
return user
|
|
91
|
+
if (!user.data)
|
|
92
|
+
return err(HaiCommonError.NOT_FOUND, serverM('notFound'))
|
|
93
|
+
const roles = await iam.authz.getUserRoles(id)
|
|
94
|
+
if (!roles.success)
|
|
95
|
+
return roles
|
|
96
|
+
return ok(toAdminUser(user.data, roles.data))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function toAdminUser(user: User, roles: Role[]): AdminUser {
|
|
100
|
+
return {
|
|
101
|
+
id: user.id,
|
|
102
|
+
username: user.username,
|
|
103
|
+
email: user.email,
|
|
104
|
+
displayName: user.displayName,
|
|
105
|
+
avatarUrl: user.avatarUrl,
|
|
106
|
+
accountType: user.accountType ?? 'permanent',
|
|
107
|
+
accountStatus: user.accountStatus,
|
|
108
|
+
accountExpiresAt: user.accountExpiresAt?.toISOString() ?? null,
|
|
109
|
+
lastLoginAt: user.lastLoginAt?.toISOString() ?? null,
|
|
110
|
+
roles: roles.map(role => role.code).filter(isRoleCode),
|
|
111
|
+
createdAt: user.createdAt.toISOString(),
|
|
112
|
+
updatedAt: user.updatedAt.toISOString(),
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function isRoleCode(code: string): code is AdminRoleCode {
|
|
117
|
+
return ROLE_CODES.has(code as AdminRoleCode)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function createAdminUser(input: AdminCreateUser, context: ServContext): Promise<HaiResult<AdminUser>> {
|
|
121
|
+
const role = await iam.authz.getRoleByCode(input.role)
|
|
122
|
+
if (!role.success)
|
|
123
|
+
return role
|
|
124
|
+
if (!role.data)
|
|
125
|
+
return err(HaiCommonError.NOT_FOUND, serverM('notFound', { locale: context.locale }))
|
|
126
|
+
const registered = await iam.user.createUser({ ...input, accountExpiresAt: input.accountExpiresAt ? new Date(input.accountExpiresAt) : undefined })
|
|
127
|
+
if (!registered.success)
|
|
128
|
+
return registered
|
|
129
|
+
const userId = registered.data.user.id
|
|
130
|
+
const assigned = await iam.authz.syncRoles(userId, [role.data.id])
|
|
131
|
+
if (!assigned.success) {
|
|
132
|
+
await rollbackCreatedUser(userId)
|
|
133
|
+
return assigned
|
|
134
|
+
}
|
|
135
|
+
await recordAudit(context, 'create', 'users', userId, { role: input.role, accountStatus: input.accountStatus })
|
|
136
|
+
return getAdminUser(userId)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 安全通知发送后发生未知结果时保留账号,不自动重试或删除已发出的邀请。 */
|
|
140
|
+
export async function inviteAdminUser(input: AdminInviteUser, context: ServContext): Promise<HaiResult<AdminUser>> {
|
|
141
|
+
const role = await iam.authz.getRoleByCode(input.role)
|
|
142
|
+
if (!role.success || !role.data)
|
|
143
|
+
return role.success ? err(HaiCommonError.NOT_FOUND, serverM('notFound', { locale: context.locale })) : role
|
|
144
|
+
const created = await iam.user.inviteUser({ ...input, accountExpiresAt: input.accountExpiresAt ? new Date(input.accountExpiresAt) : undefined })
|
|
145
|
+
if (!created.success)
|
|
146
|
+
return created
|
|
147
|
+
const assigned = await iam.authz.syncRoles(created.data.user.id, [role.data.id])
|
|
148
|
+
if (!assigned.success)
|
|
149
|
+
return assigned
|
|
150
|
+
await recordAudit(context, 'invite', 'users', created.data.user.id, { role: input.role })
|
|
151
|
+
return getAdminUser(created.data.user.id)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function rollbackCreatedUser(id: string): Promise<void> {
|
|
155
|
+
const removed = await iam.user.deleteUser(id)
|
|
156
|
+
if (!removed.success)
|
|
157
|
+
core.logger.error('Failed to roll back user creation', { userId: id, error: removed.error.code })
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function updateAdminUser(input: AdminUpdateUser, context: ServContext): Promise<HaiResult<AdminUser>> {
|
|
161
|
+
if (input.id === context.session?.userId && ((input.role && input.role !== 'admin') || (input.accountStatus && input.accountStatus !== 'active') || input.accountType === 'temporary' || input.accountExpiresAt != null))
|
|
162
|
+
return err(HaiCommonError.FORBIDDEN, serverM('selfProtection', { locale: context.locale }))
|
|
163
|
+
const existing = await iam.user.getUser(input.id)
|
|
164
|
+
if (!existing.success)
|
|
165
|
+
return existing
|
|
166
|
+
if (!existing.data)
|
|
167
|
+
return err(HaiCommonError.NOT_FOUND, serverM('notFound', { locale: context.locale }))
|
|
168
|
+
const role = input.role ? await iam.authz.getRoleByCode(input.role) : undefined
|
|
169
|
+
if (role && !role.success)
|
|
170
|
+
return role
|
|
171
|
+
if (role?.success && !role.data)
|
|
172
|
+
return err(HaiCommonError.NOT_FOUND, serverM('notFound', { locale: context.locale }))
|
|
173
|
+
const { id, role: _role, ...profile } = input
|
|
174
|
+
const previousRoles = await iam.authz.getUserRoles(id)
|
|
175
|
+
if (!previousRoles.success)
|
|
176
|
+
return previousRoles
|
|
177
|
+
if (role?.success && role.data) {
|
|
178
|
+
const assigned = await iam.authz.syncRoles(id, [role.data.id])
|
|
179
|
+
if (!assigned.success)
|
|
180
|
+
return assigned
|
|
181
|
+
}
|
|
182
|
+
const updated = await iam.user.updateUser(id, { ...profile, accountExpiresAt: profile.accountExpiresAt === undefined ? undefined : profile.accountExpiresAt ? new Date(profile.accountExpiresAt) : null })
|
|
183
|
+
if (!updated.success) {
|
|
184
|
+
// 角色 ID 可精确还原,避免 undefined 资料字段被 updateUser 忽略后无法补偿。
|
|
185
|
+
if (role?.success && role.data) {
|
|
186
|
+
const restored = await iam.authz.syncRoles(id, previousRoles.data.map(item => item.id))
|
|
187
|
+
if (!restored.success)
|
|
188
|
+
core.logger.error('Failed to restore roles after user update failure', { userId: id, error: restored.error.code })
|
|
189
|
+
}
|
|
190
|
+
return updated
|
|
191
|
+
}
|
|
192
|
+
await recordAudit(context, 'update', 'users', id, { fields: Object.keys(profile), role: input.role })
|
|
193
|
+
return getAdminUser(id)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function getAdminRoles(): Promise<HaiResult<{ items: AdminRole[] }>> {
|
|
197
|
+
const roles = await iam.authz.getAllRoles({ page: 1, pageSize: 100 })
|
|
198
|
+
if (!roles.success)
|
|
199
|
+
return roles
|
|
200
|
+
const items = roles.data.items.filter(role => isRoleCode(role.code))
|
|
201
|
+
const counts = await iam.authz.getRoleUserCounts(items.map(role => role.id))
|
|
202
|
+
if (!counts.success)
|
|
203
|
+
return counts
|
|
204
|
+
return ok({ items: items.map(role => ({
|
|
205
|
+
id: role.id,
|
|
206
|
+
code: role.code as AdminRoleCode,
|
|
207
|
+
name: role.name,
|
|
208
|
+
description: role.description,
|
|
209
|
+
userCount: counts.data.get(role.id) ?? 0,
|
|
210
|
+
})) })
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export async function updateProfile(input: ProfileUpdate, context: ServContext): Promise<HaiResult<AdminUser>> {
|
|
214
|
+
const token = context.accessToken ?? ''
|
|
215
|
+
const current = await iam.user.getCurrentUser(token)
|
|
216
|
+
if (!current.success)
|
|
217
|
+
return current
|
|
218
|
+
if ((input.locale && !(SUPPORTED_LOCALES as readonly string[]).includes(input.locale)) || (input.theme && !SUPPORTED_THEMES.includes(input.theme)))
|
|
219
|
+
return err(HaiCommonError.VALIDATION_ERROR, serverM('unsupportedPreference', { locale: context.locale }))
|
|
220
|
+
const metadata = { ...current.data.metadata }
|
|
221
|
+
if (input.locale)
|
|
222
|
+
metadata.locale = input.locale
|
|
223
|
+
if (input.theme)
|
|
224
|
+
metadata.theme = input.theme
|
|
225
|
+
const updated = await iam.user.updateCurrentUser(token, { displayName: input.displayName, email: input.email, metadata })
|
|
226
|
+
if (!updated.success)
|
|
227
|
+
return updated
|
|
228
|
+
await recordAudit(context, 'update', 'profile', current.data.id, { fields: Object.keys(input) })
|
|
229
|
+
return getAdminUser(current.data.id)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** 上传路径由服务端生成,禁止客户端写入任意 storage key。 */
|
|
233
|
+
export async function uploadAvatar(userId: string, input: AvatarUpload, context: ServContext): Promise<HaiResult<{ avatarUrl: string }>> {
|
|
234
|
+
const current = await iam.user.getUser(userId)
|
|
235
|
+
if (!current.success)
|
|
236
|
+
return current
|
|
237
|
+
if (!current.data)
|
|
238
|
+
return err(HaiCommonError.NOT_FOUND, serverM('notFound', { locale: context.locale }))
|
|
239
|
+
const bytes = Buffer.from(input.base64, 'base64')
|
|
240
|
+
if (bytes.length === 0 || bytes.length > 512 * 1024 || bytes.toString('base64') !== input.base64 || !matchesImage(bytes, input.contentType))
|
|
241
|
+
return err(HaiCommonError.VALIDATION_ERROR, serverM('invalidAvatar', { locale: context.locale }))
|
|
242
|
+
const extension = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/webp': 'webp' }[input.contentType]
|
|
243
|
+
const key = `avatars/${userId}/${core.id.uuid()}.${extension}`
|
|
244
|
+
const stored = await storage.file.put(key, bytes, { contentType: input.contentType })
|
|
245
|
+
if (!stored.success)
|
|
246
|
+
return stored
|
|
247
|
+
const apiPrefix = core.config.getOrThrow<ServConfig>('serv').http.apiPrefix
|
|
248
|
+
// 只保存生成资源路径,公开 Origin 由客户端已配置的 API 地址决定,禁止信任 Host。
|
|
249
|
+
const avatarUrl = `${apiPrefix}/assets/${key}`
|
|
250
|
+
const updated = await iam.user.updateUser(userId, { avatarUrl })
|
|
251
|
+
if (!updated.success) {
|
|
252
|
+
const cleaned = await storage.file.delete(key)
|
|
253
|
+
if (!cleaned.success)
|
|
254
|
+
core.logger.warn('Failed to clean up rejected avatar', { error: cleaned.error.code })
|
|
255
|
+
return updated
|
|
256
|
+
}
|
|
257
|
+
// 更新成功后删除旧头像;只清理此用户且符合服务端生成规则的资源。
|
|
258
|
+
const oldKey = current.data.avatarUrl?.split('/assets/')[1]
|
|
259
|
+
if (oldKey && oldKey.startsWith(`avatars/${userId}/`) && /^avatars\/[\w-]+\/[\w-]+\.(?:png|jpg|webp)$/.test(oldKey)) {
|
|
260
|
+
const cleaned = await storage.file.delete(oldKey)
|
|
261
|
+
if (!cleaned.success)
|
|
262
|
+
core.logger.warn('Failed to clean up previous avatar', { error: cleaned.error.code })
|
|
263
|
+
}
|
|
264
|
+
await recordAudit(context, 'update', 'avatars', userId)
|
|
265
|
+
return ok({ avatarUrl })
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function matchesImage(bytes: Buffer, contentType: AvatarUpload['contentType']): boolean {
|
|
269
|
+
if (contentType === 'image/png') {
|
|
270
|
+
if (bytes.length < 33 || bytes.subarray(0, 8).toString('hex') !== '89504e470d0a1a0a' || bytes.toString('ascii', 12, 16) !== 'IHDR')
|
|
271
|
+
return false
|
|
272
|
+
const width = bytes.readUInt32BE(16)
|
|
273
|
+
const height = bytes.readUInt32BE(20)
|
|
274
|
+
return width > 0 && height > 0 && width <= 4096 && height <= 4096
|
|
275
|
+
}
|
|
276
|
+
if (contentType === 'image/jpeg')
|
|
277
|
+
return bytes.length >= 4 && bytes.subarray(0, 3).toString('hex') === 'ffd8ff' && bytes.subarray(-2).toString('hex') === 'ffd9'
|
|
278
|
+
return bytes.length >= 20 && bytes.toString('ascii', 0, 4) === 'RIFF' && bytes.toString('ascii', 8, 12) === 'WEBP' && bytes.readUInt32LE(4) + 8 === bytes.length
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** 审计记录仅含操作字段与资源标识,不记录密码、token、头像内容或联系信息。 */
|
|
282
|
+
export async function recordAudit(context: ServContext, action: string, resource: string, resourceId?: string, details?: Record<string, unknown>): Promise<void> {
|
|
283
|
+
const result = await audit.log({
|
|
284
|
+
userId: context.session?.userId,
|
|
285
|
+
action,
|
|
286
|
+
resource,
|
|
287
|
+
resourceId,
|
|
288
|
+
details,
|
|
289
|
+
ipAddress: context.ip,
|
|
290
|
+
userAgent: context.userAgent,
|
|
291
|
+
})
|
|
292
|
+
if (!result.success)
|
|
293
|
+
core.logger.error('Failed to record audit event', { action, resource, error: result.error.code })
|
|
294
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** 应用自己的定时任务装配;Scheduler 先于 IAM/Audit 关闭,不修改全局回调所属关系。 */
|
|
2
|
+
import type { HaiResult } from '@h-ai/core'
|
|
3
|
+
import { audit } from '@h-ai/audit'
|
|
4
|
+
import { core, err, ok } from '@h-ai/core'
|
|
5
|
+
import { HaiSchedulerError, scheduler, SchedulerConfigSchema } from '@h-ai/scheduler'
|
|
6
|
+
import { serverM } from './messages.js'
|
|
7
|
+
|
|
8
|
+
/** 仅启用自动备份时启动现有调度器;手动备份不需要启动它。 */
|
|
9
|
+
export async function startAuditJobs(): Promise<HaiResult<void>> {
|
|
10
|
+
const schedule = audit.schedulerPlan
|
|
11
|
+
if (!schedule)
|
|
12
|
+
return ok(undefined)
|
|
13
|
+
const config = SchedulerConfigSchema.safeParse(core.config.get('scheduler') ?? {})
|
|
14
|
+
if (!config.success)
|
|
15
|
+
return err(HaiSchedulerError.CONFIG_ERROR, serverM('busy'), config.error)
|
|
16
|
+
const initialized = await scheduler.init({
|
|
17
|
+
...config.data,
|
|
18
|
+
...schedule,
|
|
19
|
+
})
|
|
20
|
+
if (!initialized.success) {
|
|
21
|
+
await scheduler.close()
|
|
22
|
+
return initialized
|
|
23
|
+
}
|
|
24
|
+
const started = scheduler.start()
|
|
25
|
+
if (!started.success)
|
|
26
|
+
await scheduler.close()
|
|
27
|
+
return started
|
|
28
|
+
}
|
package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/business-settings.ts.hbs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** 示例业务配置仅管理公开公告,不接触系统凭据或安全策略。 */
|
|
2
|
+
import type { HaiResult } from '@h-ai/core'
|
|
3
|
+
import { audit } from '@h-ai/audit'
|
|
4
|
+
import { err, HaiCommonError, ok } from '@h-ai/core'
|
|
5
|
+
import { reldb } from '@h-ai/reldb'
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
|
|
8
|
+
export const BusinessSettingsSchema = z.object({ notice: z.string().trim().max(200) }).strict()
|
|
9
|
+
|
|
10
|
+
export function initBusinessSettings(): Promise<HaiResult<void>> {
|
|
11
|
+
return reldb.ddl.createTable('app_business_settings', { id: { type: 'TEXT', primaryKey: true }, notice: { type: 'TEXT', notNull: true } })
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function readBusinessSettings(): Promise<HaiResult<{ notice: string }>> {
|
|
15
|
+
const row = await reldb.sql.get<{ notice: string }>('SELECT notice FROM app_business_settings WHERE id = ?', ['public'])
|
|
16
|
+
return row.success ? ok({ notice: row.data?.notice ?? '' }) : row
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function saveBusinessSettings(input: { notice: string }): Promise<HaiResult<{ notice: string }>> {
|
|
20
|
+
const parsed = BusinessSettingsSchema.safeParse(input)
|
|
21
|
+
if (!parsed.success)
|
|
22
|
+
return err(HaiCommonError.VALIDATION_ERROR)
|
|
23
|
+
const begun = await reldb.tx.begin()
|
|
24
|
+
if (!begun.success)
|
|
25
|
+
return begun
|
|
26
|
+
const tx = begun.data
|
|
27
|
+
try {
|
|
28
|
+
const sql = reldb.config?.type === 'mysql'
|
|
29
|
+
? 'INSERT INTO app_business_settings (id, notice) VALUES (?, ?) ON DUPLICATE KEY UPDATE notice = VALUES(notice)'
|
|
30
|
+
: 'INSERT INTO app_business_settings (id, notice) VALUES (?, ?) ON CONFLICT (id) DO UPDATE SET notice = excluded.notice'
|
|
31
|
+
const saved = await tx.execute(sql, ['public', parsed.data.notice])
|
|
32
|
+
if (!saved.success) {
|
|
33
|
+
await tx.rollback()
|
|
34
|
+
return saved
|
|
35
|
+
}
|
|
36
|
+
const logged = await audit.capture({ action: 'business.configuration.update', resource: 'business-settings', category: 'business', details: { fields: ['notice'] } }, { tx })
|
|
37
|
+
if (!logged.success) {
|
|
38
|
+
await tx.rollback()
|
|
39
|
+
return logged
|
|
40
|
+
}
|
|
41
|
+
const committed = await tx.commit()
|
|
42
|
+
if (!committed.success)
|
|
43
|
+
await tx.rollback()
|
|
44
|
+
return committed.success ? ok(parsed.data) : committed
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
await tx.rollback()
|
|
48
|
+
return err(HaiCommonError.INTERNAL_ERROR, undefined, error)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { core } from '@h-ai/core'
|
|
2
|
+
|
|
3
|
+
/** 服务端业务提示与角色种子名称同步维护中英日文。 */
|
|
4
|
+
export const serverM = core.i18n.createMessageGetter({
|
|
5
|
+
'zh-CN': {
|
|
6
|
+
admin: '系统管理员',
|
|
7
|
+
audit_admin: '审计管理员',
|
|
8
|
+
config_admin: '配置管理员',
|
|
9
|
+
dutyProtected: '不能通过此入口修改其他职责的权限定义。',
|
|
10
|
+
user: '普通用户',
|
|
11
|
+
business_operator: '业务操作员',
|
|
12
|
+
notFound: '用户或角色不存在',
|
|
13
|
+
unauthorized: '请提供要撤销的访问令牌',
|
|
14
|
+
selfProtection: '不能删除当前管理员或修改其职责、有效期和账号状态',
|
|
15
|
+
invalidAvatar: '请选择不超过 512 KB 的有效 PNG、JPEG 或 WebP 图片',
|
|
16
|
+
busy: '系统繁忙,请稍后重试',
|
|
17
|
+
unsupportedPreference: '不支持此语言或主题',
|
|
18
|
+
invalidDateRange: '结束时间不能早于开始时间',
|
|
19
|
+
},
|
|
20
|
+
'ja-JP': {
|
|
21
|
+
admin: 'システム管理者',
|
|
22
|
+
audit_admin: '監査管理者',
|
|
23
|
+
config_admin: '設定管理者',
|
|
24
|
+
dutyProtected: 'この操作では他の職務の権限定義を変更できません。',
|
|
25
|
+
user: '一般ユーザー',
|
|
26
|
+
business_operator: '業務担当者',
|
|
27
|
+
notFound: 'ユーザーまたはロールが存在しません',
|
|
28
|
+
unauthorized: '取り消すアクセストークンを指定してください',
|
|
29
|
+
selfProtection: '自分の管理者アカウントの削除や職務、有効期限、状態の変更はできません',
|
|
30
|
+
invalidAvatar: '512KB以下の有効なPNG、JPEG、WebP画像を選択してください',
|
|
31
|
+
busy: 'システムが混雑しています。再試行してください',
|
|
32
|
+
unsupportedPreference: 'この言語またはテーマには対応していません',
|
|
33
|
+
invalidDateRange: '終了日は開始日以降にしてください',
|
|
34
|
+
},
|
|
35
|
+
'en-US': {
|
|
36
|
+
admin: 'System administrator',
|
|
37
|
+
audit_admin: 'Audit administrator',
|
|
38
|
+
config_admin: 'Configuration administrator',
|
|
39
|
+
dutyProtected: 'This operation cannot change permission definitions for other duties.',
|
|
40
|
+
user: 'User',
|
|
41
|
+
business_operator: 'Business operator',
|
|
42
|
+
notFound: 'User or role does not exist',
|
|
43
|
+
unauthorized: 'Provide the access token to revoke',
|
|
44
|
+
selfProtection: 'Ask another administrator to change your duty, expiry or account status',
|
|
45
|
+
invalidAvatar: 'Choose a valid PNG, JPEG or WebP image up to 512 KB',
|
|
46
|
+
busy: 'The system is busy. Please try again',
|
|
47
|
+
unsupportedPreference: 'This language or theme is not supported',
|
|
48
|
+
invalidDateRange: 'End date must not precede start date',
|
|
49
|
+
},
|
|
50
|
+
})
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { ServContext } from '@h-ai/serv'
|
|
2
|
+
import { audit, AuditInitConfigSchema } from '@h-ai/audit'
|
|
3
|
+
import { core, err, HaiCommonError, ok } from '@h-ai/core'
|
|
4
|
+
import { iam } from '@h-ai/iam'
|
|
5
|
+
import { serv } from '@h-ai/serv'
|
|
6
|
+
import { createIamProcedures } from '@h-ai/serv/features/iam'
|
|
7
|
+
import { adminContract, profileContract } from '{{fullstack.contractPackageName}}'
|
|
8
|
+
import {
|
|
9
|
+
createAdminUser,
|
|
10
|
+
getAdminRoles,
|
|
11
|
+
inviteAdminUser,
|
|
12
|
+
isRoleCode,
|
|
13
|
+
recordAudit,
|
|
14
|
+
toAdminUser,
|
|
15
|
+
updateAdminUser,
|
|
16
|
+
updateProfile,
|
|
17
|
+
uploadAvatar,
|
|
18
|
+
withAdminMutation,
|
|
19
|
+
} from '../admin-service.js'
|
|
20
|
+
import { readBusinessSettings, saveBusinessSettings } from '../business-settings.js'
|
|
21
|
+
import { serverM } from '../messages.js'
|
|
22
|
+
|
|
23
|
+
/** 身份认证沿用框架公共契约与 refresh-cookie 传输。 */
|
|
24
|
+
export function createAuthProcedures() {
|
|
25
|
+
return createIamProcedures({ iam }).auth
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createAdminProcedures() {
|
|
29
|
+
return serv.implement(adminContract).context<ServContext>().route('permissions.list').permission('app.roles.manage').handle(async ({ input }) => {
|
|
30
|
+
const result = await iam.authz.getAllPermissions(input)
|
|
31
|
+
return result.success ? ok({ total: result.data.total, items: result.data.items.map(item => ({ id: item.id, code: item.code, name: item.name, sensitiveOperations: item.sensitiveOperations ?? [] })) }) : result
|
|
32
|
+
}).route('permissions.update').permission('app.roles.manage').handle(({ input, context }) => withAdminMutation(context, 'iam.permissions.sensitiveOperations', async () => {
|
|
33
|
+
const result = await iam.authz.setPermissionSensitiveOperations(input.id, input.sensitiveOperations)
|
|
34
|
+
return result.success ? ok({ saved: true as const }) : result
|
|
35
|
+
})).route('configuration.get').permission('app.config.manage').handle(() => readBusinessSettings()).route('configuration.update').permission('app.config.manage').handle(async ({ input, context }) => {
|
|
36
|
+
const checked = await iam.auth.checkSensitiveOperation(context.accessToken ?? '', 'business.settings.update', context.request.headers.get('x-hai-reauthentication') ?? undefined)
|
|
37
|
+
return checked.success ? saveBusinessSettings(input) : checked
|
|
38
|
+
}).route('users.list').permission('app.users.manage').handle(async ({ input }) => {
|
|
39
|
+
const users = await iam.user.listUsers(input)
|
|
40
|
+
if (!users.success)
|
|
41
|
+
return users
|
|
42
|
+
const roles = await iam.authz.getUserRolesForMany(users.data.items.map(user => user.id))
|
|
43
|
+
if (!roles.success)
|
|
44
|
+
return roles
|
|
45
|
+
return ok({ ...users.data, items: users.data.items.map(user => toAdminUser(user, roles.data.get(user.id) ?? [])) })
|
|
46
|
+
}).route('users.resetPassword').permission('app.users.manage').handle(({ input, context }) => withAdminMutation(context, 'iam.users.resetPassword', async () => {
|
|
47
|
+
const result = await iam.user.adminResetPassword(input.id, input.newPassword)
|
|
48
|
+
if (!result.success)
|
|
49
|
+
return result
|
|
50
|
+
await recordAudit(context, 'password_reset', 'users', input.id)
|
|
51
|
+
return ok({ reset: true as const })
|
|
52
|
+
})).route('users.invite').permission('app.users.manage').handle(({ input, context }) => withAdminMutation(context, 'iam.users.invite', () => inviteAdminUser(input, context))).route('users.create').permission('app.users.manage').handle(({ input, context }) => withAdminMutation(context, 'iam.users.create', () => createAdminUser(input, context))).route('users.update').permission('app.users.manage').handle(({ input, context }) => withAdminMutation(context, 'iam.users.update', () => updateAdminUser(input, context))).route('users.delete').permission('app.users.manage').handle(({ input, context }) => withAdminMutation(context, 'iam.users.delete', async () => {
|
|
53
|
+
if (context.session.userId === input.id)
|
|
54
|
+
return err(HaiCommonError.FORBIDDEN, serverM('selfProtection', { locale: context.locale }))
|
|
55
|
+
const result = await iam.user.deleteUser(input.id)
|
|
56
|
+
if (!result.success)
|
|
57
|
+
return result
|
|
58
|
+
await recordAudit(context, 'delete', 'users', input.id)
|
|
59
|
+
return ok({ deleted: true as const })
|
|
60
|
+
})).route('users.avatar').permission('app.users.manage').handle(({ input, context }) => withAdminMutation(context, 'iam.users.avatar', () => uploadAvatar(input.id, input, context))).route('roles.list').permission('app.roles.manage').handle(() => getAdminRoles()).route('roles.update').permission('app.roles.manage').handle(({ input, context }) => withAdminMutation(context, 'iam.roles.update', async () => {
|
|
61
|
+
const role = await iam.authz.getRole(input.id)
|
|
62
|
+
if (!role.success)
|
|
63
|
+
return role
|
|
64
|
+
if (!role.data || !isRoleCode(role.data.code))
|
|
65
|
+
return err(HaiCommonError.NOT_FOUND, serverM('notFound', { locale: context.locale }))
|
|
66
|
+
if (role.data.code !== 'admin' && role.data.code !== 'user')
|
|
67
|
+
return err(HaiCommonError.FORBIDDEN, serverM('dutyProtected', { locale: context.locale }))
|
|
68
|
+
const updated = await iam.authz.updateRole(input.id, { description: input.description })
|
|
69
|
+
if (!updated.success)
|
|
70
|
+
return updated
|
|
71
|
+
const roles = await getAdminRoles()
|
|
72
|
+
if (!roles.success)
|
|
73
|
+
return roles
|
|
74
|
+
const result = roles.data.items.find(item => item.id === input.id)
|
|
75
|
+
if (!result)
|
|
76
|
+
return err(HaiCommonError.NOT_FOUND, serverM('notFound', { locale: context.locale }))
|
|
77
|
+
await recordAudit(context, 'update', 'roles', input.id, { fields: ['description'] })
|
|
78
|
+
return ok(result)
|
|
79
|
+
})).route('audit.backup').permission('audit:backup').handle(async ({ input, context }) => {
|
|
80
|
+
const checked = await iam.auth.checkSensitiveOperation(context.accessToken ?? '', 'audit.backup', context.request.headers.get('x-hai-reauthentication') ?? undefined)
|
|
81
|
+
if (!checked.success)
|
|
82
|
+
return checked
|
|
83
|
+
const result = await audit.backup.run({ ...input, signal: context.request.signal })
|
|
84
|
+
return result.success ? ok({ ...result.data, createdAt: result.data.createdAt.toISOString(), cutoff: result.data.cutoff.toISOString() }) : result
|
|
85
|
+
}).route('audit.list').permission('audit:read').handle(async ({ input, context }) => {
|
|
86
|
+
if (input.startDate && input.endDate && input.startDate > input.endDate)
|
|
87
|
+
return err(HaiCommonError.VALIDATION_ERROR, serverM('invalidDateRange', { locale: context.locale }))
|
|
88
|
+
const result = await audit.list({ ...input, startDate: input.startDate ? new Date(input.startDate) : undefined, endDate: input.endDate ? new Date(input.endDate) : undefined })
|
|
89
|
+
if (!result.success)
|
|
90
|
+
return result
|
|
91
|
+
const stats = await audit.getStats(7, { category: input.category, outcome: input.outcome, severity: input.severity, resource: input.resource, scopeId: input.scopeId, userId: input.userId })
|
|
92
|
+
if (!stats.success)
|
|
93
|
+
return stats
|
|
94
|
+
return ok({ ...result.data, stats: stats.data, backupEnabled: AuditInitConfigSchema.parse(core.config.get('audit') ?? {}).backup.enabled, items: result.data.items.map(item => ({ ...item, createdAt: new Date(item.createdAt).toISOString() })) })
|
|
95
|
+
}).build()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function createProfileProcedures() {
|
|
99
|
+
return serv.implement(profileContract).context<ServContext>().route('update').auth().handle(async ({ input, context }) => {
|
|
100
|
+
const checked = await iam.auth.checkSensitiveOperation(context.accessToken ?? '', 'iam.profile.update', context.request.headers.get('x-hai-reauthentication') ?? undefined)
|
|
101
|
+
return checked.success ? updateProfile(input, context) : checked
|
|
102
|
+
}).route('avatar').auth().handle(async ({ input, context }) => {
|
|
103
|
+
const checked = await iam.auth.checkSensitiveOperation(context.accessToken ?? '', 'iam.profile.avatar', context.request.headers.get('x-hai-reauthentication') ?? undefined)
|
|
104
|
+
return checked.success ? uploadAvatar(context.session.userId, input, context) : checked
|
|
105
|
+
}).build()
|
|
106
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ServContext } from '@h-ai/serv'
|
|
2
|
+
import type { AppEchoOutputData, AppFrontendTarget, AppInfoOutputData } from '{{fullstack.contractPackageName}}'
|
|
3
|
+
import { ok } from '@h-ai/core'
|
|
4
|
+
import { serv } from '@h-ai/serv'
|
|
5
|
+
import { appContract } from '{{fullstack.contractPackageName}}'
|
|
6
|
+
import { readBusinessSettings } from '../business-settings.js'
|
|
7
|
+
|
|
8
|
+
export interface CreateAppProceduresOptions {
|
|
9
|
+
readonly name: string
|
|
10
|
+
readonly version: string
|
|
11
|
+
readonly env: string
|
|
12
|
+
readonly defaultLocale: string
|
|
13
|
+
readonly supportedLocales: readonly string[]
|
|
14
|
+
readonly startedAt: string
|
|
15
|
+
readonly frontends: readonly AppFrontendTarget[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** app 领域 procedures:把共享 contract 绑定到真实业务处理函数。 */
|
|
19
|
+
export function createAppProcedures(options: CreateAppProceduresOptions) {
|
|
20
|
+
return serv
|
|
21
|
+
.implement(appContract)
|
|
22
|
+
.context<ServContext>()
|
|
23
|
+
.route('info', async () => {
|
|
24
|
+
const settings = await readBusinessSettings()
|
|
25
|
+
return settings.success ? ok({ ...createAppInfo(options), notice: settings.data.notice }) : settings
|
|
26
|
+
})
|
|
27
|
+
.route('echo', ({ input, context }) => {
|
|
28
|
+
const data: AppEchoOutputData = {
|
|
29
|
+
message: input.message,
|
|
30
|
+
echoedAt: new Date().toISOString(),
|
|
31
|
+
requestId: context.requestId,
|
|
32
|
+
}
|
|
33
|
+
return ok(data)
|
|
34
|
+
})
|
|
35
|
+
.build()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createAppInfo(options: CreateAppProceduresOptions): AppInfoOutputData {
|
|
39
|
+
return {
|
|
40
|
+
notice: '',
|
|
41
|
+
name: options.name,
|
|
42
|
+
version: options.version,
|
|
43
|
+
env: options.env,
|
|
44
|
+
defaultLocale: options.defaultLocale,
|
|
45
|
+
supportedLocales: [...options.supportedLocales],
|
|
46
|
+
startedAt: options.startedAt,
|
|
47
|
+
frontends: [...options.frontends],
|
|
48
|
+
}
|
|
49
|
+
}
|
package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/index.ts.hbs
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { CreateAppProceduresOptions } from './app-procedures.js'
|
|
2
|
+
{{#if features.feedback}}
|
|
3
|
+
import { feedback } from '@h-ai/feedback'
|
|
4
|
+
import { createFeedbackProcedures } from '@h-ai/serv/features/feedback'
|
|
5
|
+
{{/if}}
|
|
6
|
+
import { createAdminProcedures, createAuthProcedures, createProfileProcedures } from './admin-procedures.js'
|
|
7
|
+
import { createAppProcedures } from './app-procedures.js'
|
|
8
|
+
|
|
9
|
+
// @hai-generator:procedures:imports
|
|
10
|
+
|
|
11
|
+
export interface CreateServerProceduresOptions {
|
|
12
|
+
readonly app: CreateAppProceduresOptions
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** 工程级 procedures 聚合入口;后续增加领域时在这里追加即可。 */
|
|
16
|
+
export function createServerProcedures(options: CreateServerProceduresOptions) {
|
|
17
|
+
return {
|
|
18
|
+
// @hai-generator:procedures:entries
|
|
19
|
+
app: createAppProcedures(options.app),
|
|
20
|
+
iam: { auth: createAuthProcedures() },
|
|
21
|
+
admin: createAdminProcedures(),
|
|
22
|
+
profile: createProfileProcedures(),
|
|
23
|
+
{{#if features.feedback}}
|
|
24
|
+
feedback: createFeedbackProcedures({
|
|
25
|
+
feedback,
|
|
26
|
+
resolveContext: ({ session }) => ({
|
|
27
|
+
scopeId: '{{projectName}}',
|
|
28
|
+
userId: session.userId,
|
|
29
|
+
permissions: session.permissions,
|
|
30
|
+
}),
|
|
31
|
+
}),
|
|
32
|
+
{{/if}}
|
|
33
|
+
}
|
|
34
|
+
}
|