@team-codeme/crud-forge 0.0.0-stage → 1.0.0
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/.cursor/agents/generator-engineer.md +31 -0
- package/.cursor/agents/keycloak-engineer.md +33 -0
- package/.cursor/agents/qa-engineer.md +32 -0
- package/.cursor/agents/security-reviewer.md +30 -0
- package/.cursor/mcp.json +13 -0
- package/.cursor/rules/crudforge-precautions.mdc +16 -0
- package/.cursor/rules/playwright-browser.mdc +30 -0
- package/.cursor/rules/readme-todo-sync.mdc +13 -0
- package/.env.qa.example +19 -0
- package/.github/workflows/qa-live.yml +32 -0
- package/.github/workflows/qa.yml +27 -0
- package/.vscode/settings.json +32 -0
- package/QA/IAM-PLAN.md +25 -0
- package/QA/IAM-RESULTS.md +39 -0
- package/QA/IAM-TESTCASES.md +36 -0
- package/QA/IAM-TODO.md +7 -0
- package/QA/KEYCLOAK-BOOTSTRAP.md +105 -0
- package/QA/PLAN.md +43 -0
- package/QA/RESULTS.md +59 -0
- package/QA/TESTCASES.md +65 -0
- package/QA/TODO.md +14 -0
- package/QA/pmb-dev/README.md +49 -0
- package/QA/pmb-dev/config/config.yml +152 -0
- package/QA/pmb-dev/gdl/i.gdl +17 -0
- package/README.md +851 -2
- package/Todo.md +207 -0
- package/assets/branding/crudforge-logo.png +0 -0
- package/assets/branding/crudforge-mark.png +0 -0
- package/branding/poster-crudforge.jpg +0 -0
- package/chat/last-chat/Investigating Mock Data Configuration.md +428 -0
- package/cli.js +167 -0
- package/config/default-config.yml +245 -0
- package/docs/performance/README.md +217 -0
- package/docs/performance/after.json +111 -0
- package/docs/performance/before.json +111 -0
- package/docs/quality/README.md +276 -0
- package/e2e/crudforge.e2e.yml +47 -0
- package/e2e/crudforge.field-profile.yml +33 -0
- package/e2e/crudforge.iam-live.yml +124 -0
- package/e2e/crudforge.iam.yml +62 -0
- package/e2e/tests/field-profile.spec.js +152 -0
- package/e2e/tests/generated-crud.spec.js +251 -0
- package/e2e/tests/iam-live.spec.js +149 -0
- package/e2e/tests/iam-pack.spec.js +151 -0
- package/e2e/tests/list-state.spec.js +73 -0
- package/features.csv +27 -0
- package/fuse-starter-pack/branding/crudforge-mark-source.jpg +0 -0
- package/fuse-starter-pack/branding/crudforge-mark-source.png +0 -0
- package/fuse-starter-pack/branding/crudforge-mark.png +0 -0
- package/fuse-starter-pack/branding/crudforge-wordmark-source.jpg +0 -0
- package/fuse-starter-pack/branding/crudforge-wordmark-source.png +0 -0
- package/fuse-starter-pack/branding/crudforge-wordmark.png +0 -0
- package/fuse-starter-pack/fuse-v21.1/Dockerfile +14 -0
- package/fuse-starter-pack/fuse-v21.1/LICENSE.md +6 -0
- package/fuse-starter-pack/fuse-v21.1/README.md +59 -0
- package/fuse-starter-pack/fuse-v21.1/angular.json +113 -0
- package/fuse-starter-pack/fuse-v21.1/eslint.config.ts +150 -0
- package/fuse-starter-pack/fuse-v21.1/nginx.conf +23 -0
- package/fuse-starter-pack/fuse-v21.1/nginx.dev.conf +18 -0
- package/fuse-starter-pack/fuse-v21.1/package-lock.json +11513 -0
- package/fuse-starter-pack/fuse-v21.1/package.json +69 -0
- package/fuse-starter-pack/fuse-v21.1/postcss.config.json +5 -0
- package/fuse-starter-pack/fuse-v21.1/prettier.config.ts +15 -0
- package/fuse-starter-pack/fuse-v21.1/proxy.conf.json +8 -0
- package/fuse-starter-pack/fuse-v21.1/proxy.dev.conf.json +8 -0
- package/fuse-starter-pack/fuse-v21.1/public/favicon.png +0 -0
- package/fuse-starter-pack/fuse-v21.1/public/fonts/geist/Geist-Italic.woff2 +0 -0
- package/fuse-starter-pack/fuse-v21.1/public/fonts/geist/Geist.woff2 +0 -0
- package/fuse-starter-pack/fuse-v21.1/public/fonts/geist/GeistMono-Italic.woff2 +0 -0
- package/fuse-starter-pack/fuse-v21.1/public/fonts/geist/GeistMono.woff2 +0 -0
- package/fuse-starter-pack/fuse-v21.1/public/i18n/en.json +8 -0
- package/fuse-starter-pack/fuse-v21.1/public/i18n/es.json +8 -0
- package/fuse-starter-pack/fuse-v21.1/public/images/flags/ES.svg +79 -0
- package/fuse-starter-pack/fuse-v21.1/public/images/flags/US.svg +14 -0
- package/fuse-starter-pack/fuse-v21.1/public/images/flags/where-to-find-other-flags.txt +5 -0
- package/fuse-starter-pack/fuse-v21.1/public/images/logo/logo-mark.png +0 -0
- package/fuse-starter-pack/fuse-v21.1/public/images/logo/logo.png +0 -0
- package/fuse-starter-pack/fuse-v21.1/push.sh +27 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/app.config.server.ts +10 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/app.config.ts +64 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/app.routes.server.ts +8 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/app.routes.ts +39 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/app.ts +22 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/icons/index.ts +1 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/icons/lucide.ts +1 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/icons/provider.ts +23 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/local-storage/index.ts +2 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/local-storage/local-storage.ts +109 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/local-storage/provider.ts +15 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/media/index.ts +2 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/media/media.ts +64 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/media/provider.ts +15 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/index.ts +3 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/models/theming.ts +13 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/palette.ts +209 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/provider.ts +24 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/theming.ts +129 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/transloco/transloco-http-loader.ts +12 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/data/navigation.ts +176 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/layout.ts +77 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/language-switcher.ts +91 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/navigation.ts +238 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/notifications.ts +252 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/scheme-switcher.ts +58 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/sidebar.ts +31 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/user.ts +132 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/data/academy.ts +753 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/data/model.ts +27 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/features/course.ts +318 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/features/courses.ts +228 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/routes.ts +14 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/data/contacts.ts +4372 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/data/model.ts +34 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/features/contact.ts +239 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/features/contacts.ts +185 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/routes.ts +16 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/data/file-manager.ts +261 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/data/model.ts +12 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/features/file-manager.ts +131 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/routes.ts +15 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/ui/list.ts +164 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/features/faq.ts +288 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/features/guide.ts +134 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/features/home.ts +219 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/features/support.ts +161 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/routes.ts +33 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/data/model.ts +18 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/data/notes.ts +324 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/features/note.ts +152 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/features/notes.ts +118 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/routes.ts +16 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/data/model.ts +17 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/data/tasks.ts +562 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/features/task.ts +195 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/features/tasks.ts +271 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/routes.ts +16 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/data/analytics.ts +1620 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/data/finance.ts +1065 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/data/project.ts +376 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/features/analytics/analytics.ts +1096 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/features/finance/finance.ts +658 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/features/project/budget-table.ts +188 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/features/project/project.ts +565 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/routes.ts +23 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/features/changelog/changelog.ts +932 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/features/getting-started/building.ts +33 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/features/getting-started/development.ts +42 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/features/getting-started/installation.ts +47 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/layout.ts +9 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/routes.ts +45 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/error/features/error-page.ts +70 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/error/routes.ts +21 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/notifications/features/notifications.ts +262 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/notifications/routes.ts +10 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/account.ts +238 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/notifications.ts +134 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/plan-and-billing.ts +238 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/security.ts +211 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/team.ts +188 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/layout.ts +109 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/routes.ts +37 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/welcome/welcome.component.ts +22 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/routes.ts +95 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/forgot-password/forgot-password.html +54 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/forgot-password/forgot-password.ts +51 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/reset-password/reset-password.html +82 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/reset-password/reset-password.ts +69 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/sign-in/sign-in.html +132 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/sign-in/sign-in.ts +54 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/sign-up/sign-up.html +183 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/sign-up/sign-up.ts +56 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/layout.ts +9 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/routes.ts +34 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/coming-soon/features/coming-soon.ts +156 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/coming-soon/layout.ts +9 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/coming-soon/routes.ts +16 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/website/layout/layout.ts +9 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/website/modules/home/features/home.ts +23 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/website/modules/home/routes.ts +10 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/website/routes.ts +30 -0
- package/fuse-starter-pack/fuse-v21.1/src/environments/environment.mock.ts +5 -0
- package/fuse-starter-pack/fuse-v21.1/src/environments/environment.prod.ts +5 -0
- package/fuse-starter-pack/fuse-v21.1/src/environments/environment.ts +5 -0
- package/fuse-starter-pack/fuse-v21.1/src/index.html +66 -0
- package/fuse-starter-pack/fuse-v21.1/src/main.server.ts +11 -0
- package/fuse-starter-pack/fuse-v21.1/src/main.ts +5 -0
- package/fuse-starter-pack/fuse-v21.1/src/server.ts +70 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/base/base.css +63 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/base/typography.css +54 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/components/a.css +18 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/components/input.css +22 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/components/kbd.css +55 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/components/material.css +1149 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/styles.css +31 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/tailwind/theme.css +147 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/tailwind/utilities.css +563 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/tailwind/variants.css +4 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/third-party/lucide-icons.css +12 -0
- package/fuse-starter-pack/fuse-v21.1/tsconfig.app.json +16 -0
- package/fuse-starter-pack/fuse-v21.1/tsconfig.json +37 -0
- package/fuse-starter-pack/fuse-v21.1/tsconfig.spec.json +10 -0
- package/generator/README.md +12 -0
- package/generator/issues/.gitkeep +0 -0
- package/package.json +56 -4
- package/playwright.config.js +29 -0
- package/playwright.field-profile.config.js +6 -0
- package/playwright.iam-live.config.js +31 -0
- package/playwright.iam.config.js +29 -0
- package/playwright.list-state.config.js +6 -0
- package/playwright.performance.config.js +1 -0
- package/run-config-tests.sh +5 -0
- package/scripts/e2e-paths.js +89 -0
- package/scripts/e2e-prepare-field-profile.js +17 -0
- package/scripts/e2e-prepare-iam-live.js +135 -0
- package/scripts/e2e-prepare-iam.js +65 -0
- package/scripts/e2e-prepare.js +72 -0
- package/scripts/e2e-serve-iam-live.js +28 -0
- package/scripts/e2e-serve-iam.js +28 -0
- package/scripts/e2e-serve.js +28 -0
- package/scripts/performance/generate.js +17 -0
- package/scripts/performance/measure.js +55 -0
- package/scripts/postinstall.js +9 -0
- package/scripts/prepare-quality-apps.js +34 -0
- package/scripts/run-config-tests.js +63 -0
- package/src/assets/logo-with-lable.jpeg +0 -0
- package/src/assets/logo.jpeg +0 -0
- package/src/commands/bootstrap.js +92 -0
- package/src/commands/doctor.js +403 -0
- package/src/commands/generate.js +301 -0
- package/src/commands/init-config.js +310 -0
- package/src/commands/keycloak-options.js +182 -0
- package/src/commands/validate.js +52 -0
- package/src/config/auth-config.js +183 -0
- package/src/config/default-values.js +235 -0
- package/src/config/env-refs.js +71 -0
- package/src/config/field-preferences.js +99 -0
- package/src/config/input-sources.js +104 -0
- package/src/config/load-config.js +166 -0
- package/src/config/proxy-routes.js +44 -0
- package/src/generator/access-control-tools.js +334 -0
- package/src/generator/ai-docs.js +691 -0
- package/src/generator/application-tests.js +252 -0
- package/src/generator/auth-pack.js +965 -0
- package/src/generator/embed-config.js +431 -0
- package/src/generator/entity-layout.js +30 -0
- package/src/generator/fuse-build.js +57 -0
- package/src/generator/fuse-runtime.js +344 -0
- package/src/generator/generated-banner.js +137 -0
- package/src/generator/index.js +874 -0
- package/src/generator/project-output.js +126 -0
- package/src/generator/team-pack.js +220 -0
- package/src/index.js +68 -0
- package/src/parser/gdl-parser.js +169 -0
- package/src/parser/jdl-parser.js +171 -0
- package/src/presets/pmb-test.js +139 -0
- package/src/presets/static-source.js +73 -0
- package/src/presets/store.js +96 -0
- package/src/provision/admin-service.js +148 -0
- package/src/provision/bootstrap.js +147 -0
- package/src/provision/http.js +123 -0
- package/src/provision/keycloak-admin-rest.js +335 -0
- package/src/provision/state.js +51 -0
- package/src/provision/token.js +52 -0
- package/src/templates/angular/auth/access-control.service.ts.ejs +179 -0
- package/src/templates/angular/auth/access-ctrl.pipe.ts.ejs +20 -0
- package/src/templates/angular/auth/auth.guard.ts.ejs +27 -0
- package/src/templates/angular/auth/auth.interceptor.ts.ejs +53 -0
- package/src/templates/angular/auth/auth.service.ts.ejs +594 -0
- package/src/templates/angular/auth/auth.utils.ts.ejs +28 -0
- package/src/templates/angular/auth/current-user.ts.ejs +24 -0
- package/src/templates/angular/auth/disabled-account-overlay.component.css.ejs +95 -0
- package/src/templates/angular/auth/disabled-account-overlay.component.html.ejs +33 -0
- package/src/templates/angular/auth/disabled-account-overlay.component.ts.ejs +23 -0
- package/src/templates/angular/auth/fuse-account.ts.ejs +234 -0
- package/src/templates/angular/auth/fuse-user.ts.ejs +264 -0
- package/src/templates/angular/auth/has-permission.directive.ts.ejs +51 -0
- package/src/templates/angular/auth/index.ts.ejs +24 -0
- package/src/templates/angular/auth/keycloak-admin.service.ts.ejs +670 -0
- package/src/templates/angular/auth/landing.guard.ts.ejs +61 -0
- package/src/templates/angular/auth/no-auth.guard.ts.ejs +19 -0
- package/src/templates/angular/auth/permission-attribute.util.ts.ejs +182 -0
- package/src/templates/angular/auth/sign-in.ts.ejs +60 -0
- package/src/templates/angular/entity/_entity_.model.ts.ejs +64 -0
- package/src/templates/angular/entity/_entity_.routes.ts.ejs +9 -0
- package/src/templates/angular/entity/components/forms/form/_entity_-form.component.html.ejs +168 -0
- package/src/templates/angular/entity/components/forms/form/_entity_-form.component.ts.ejs +175 -0
- package/src/templates/angular/entity/components/forms/form-page/_entity_-form-page.component.html.ejs +10 -0
- package/src/templates/angular/entity/components/forms/form-page/_entity_-form-page.component.ts.ejs +86 -0
- package/src/templates/angular/entity/components/forms/update/_entity_-form.service.ts.ejs +136 -0
- package/src/templates/angular/entity/components/interaction/actions/_entity_-actions.component.html.ejs +68 -0
- package/src/templates/angular/entity/components/interaction/actions/_entity_-actions.component.ts.ejs +69 -0
- package/src/templates/angular/entity/components/interaction/filter/_entity_-filter.component.html.ejs +111 -0
- package/src/templates/angular/entity/components/interaction/filter/_entity_-filter.component.ts.ejs +102 -0
- package/src/templates/angular/entity/components/presentation/card/_entity_-card.component.css.ejs +33 -0
- package/src/templates/angular/entity/components/presentation/card/_entity_-card.component.html.ejs +29 -0
- package/src/templates/angular/entity/components/presentation/card/_entity_-card.component.ts.ejs +93 -0
- package/src/templates/angular/entity/components/presentation/detail/_entity_-detail.component.css.ejs +54 -0
- package/src/templates/angular/entity/components/presentation/detail/_entity_-detail.component.html.ejs +58 -0
- package/src/templates/angular/entity/components/presentation/detail/_entity_-detail.component.ts.ejs +47 -0
- package/src/templates/angular/entity/components/presentation/list/_entity_-list.component.html.ejs +419 -0
- package/src/templates/angular/entity/components/presentation/list/_entity_-list.component.scss.ejs +48 -0
- package/src/templates/angular/entity/components/presentation/list/_entity_-list.component.ts.ejs +592 -0
- package/src/templates/angular/entity/service/_entity_.service.ts.ejs +153 -0
- package/src/templates/angular/entity/settings/_entity_.settings.ts.ejs +49 -0
- package/src/templates/angular/enums/_enum_.model.ts.ejs +11 -0
- package/src/templates/angular/iam/change-password.component.html.ejs +39 -0
- package/src/templates/angular/iam/change-password.component.ts.ejs +44 -0
- package/src/templates/angular/iam/no-access.component.ts.ejs +17 -0
- package/src/templates/angular/iam/role-list.component.html.ejs +331 -0
- package/src/templates/angular/iam/role-list.component.scss.ejs +107 -0
- package/src/templates/angular/iam/role-list.component.ts.ejs +597 -0
- package/src/templates/angular/iam/role-management/role-header.component.html.ejs +51 -0
- package/src/templates/angular/iam/role-management/role-header.component.ts.ejs +45 -0
- package/src/templates/angular/iam/role-management/role-inheritance.component.html.ejs +15 -0
- package/src/templates/angular/iam/role-management/role-inheritance.component.ts.ejs +19 -0
- package/src/templates/angular/iam/role-management/role-permissions-panel.component.css.ejs +24 -0
- package/src/templates/angular/iam/role-management/role-permissions-panel.component.html.ejs +108 -0
- package/src/templates/angular/iam/role-management/role-permissions-panel.component.ts.ejs +122 -0
- package/src/templates/angular/iam/role-management/role-users.component.html.ejs +104 -0
- package/src/templates/angular/iam/role-management/role-users.component.ts.ejs +53 -0
- package/src/templates/angular/iam/role-management/user-select-dialog.component.html.ejs +113 -0
- package/src/templates/angular/iam/role-management/user-select-dialog.component.ts.ejs +157 -0
- package/src/templates/angular/iam/role-permission-matrix.component.html.ejs +355 -0
- package/src/templates/angular/iam/role-permission-matrix.component.scss.ejs +217 -0
- package/src/templates/angular/iam/role-permission-matrix.component.ts.ejs +800 -0
- package/src/templates/angular/iam/screen-catalog.ts.ejs +147 -0
- package/src/templates/angular/iam/shared/confirm-dialog.component.ts.ejs +83 -0
- package/src/templates/angular/iam/shared/matrix-hierarchy.util.ts.ejs +196 -0
- package/src/templates/angular/iam/shared/permission-matrix-sound.ts.ejs +59 -0
- package/src/templates/angular/iam/shared/role-display.util.ts.ejs +43 -0
- package/src/templates/angular/iam/shared/role-permission-skeleton.component.ts.ejs +54 -0
- package/src/templates/angular/iam/shared/screen-matrix.util.ts.ejs +554 -0
- package/src/templates/angular/iam/shared/screen-navigation-map.ts.ejs +187 -0
- package/src/templates/angular/iam/shared/screen-navigation.registry.ts.ejs +64 -0
- package/src/templates/angular/iam/shared/text-highlight.pipe.ts.ejs +39 -0
- package/src/templates/angular/iam/user-access-matrix.component.html.ejs +212 -0
- package/src/templates/angular/iam/user-access-matrix.component.scss.ejs +34 -0
- package/src/templates/angular/iam/user-access-matrix.component.ts.ejs +239 -0
- package/src/templates/angular/iam/user-detail.component.html.ejs +143 -0
- package/src/templates/angular/iam/user-detail.component.ts.ejs +108 -0
- package/src/templates/angular/iam/user-form.component.html.ejs +63 -0
- package/src/templates/angular/iam/user-form.component.ts.ejs +100 -0
- package/src/templates/angular/iam/user-list.component.html.ejs +369 -0
- package/src/templates/angular/iam/user-list.component.scss.ejs +242 -0
- package/src/templates/angular/iam/user-list.component.ts.ejs +446 -0
- package/src/templates/angular/iam/user-management.routes.ts.ejs +47 -0
- package/src/templates/angular/iam/user-permission-matrix.component.html.ejs +231 -0
- package/src/templates/angular/iam/user-permission-matrix.component.scss.ejs +216 -0
- package/src/templates/angular/iam/user-permission-matrix.component.ts.ejs +516 -0
- package/src/templates/angular/shared/base/audit-user-name.pipe.ts.ejs +69 -0
- package/src/templates/angular/shared/base/base-filter.component.ts.ejs +245 -0
- package/src/templates/angular/shared/base/base-form.component.ts.ejs +109 -0
- package/src/templates/angular/shared/base/base-list.component.ts.ejs +507 -0
- package/src/templates/angular/shared/base/filter-types.ts.ejs +55 -0
- package/src/templates/angular/shared/base/input-format.directive.ts.ejs +47 -0
- package/src/templates/angular/shared/base/input-format.ts.ejs +15 -0
- package/src/templates/angular/shared/confirm-with-input-dialog.component.ts.ejs +91 -0
- package/src/templates/angular/shared/confirmation-dialog.component.ts.ejs +80 -0
- package/src/templates/angular/shared/list-styles.css.ejs +103 -0
- package/src/templates/angular/shared/loading-overlay.component.ts.ejs +24 -0
- package/src/templates/angular/shared/resizable.directive.ts.ejs +115 -0
- package/src/templates/angular/shared/snackbar-styles.css.ejs +11 -0
- package/src/templates/fuse-packs/access-control-tools/access-control-sync.js +819 -0
- package/src/templates/fuse-packs/access-control-tools/build-rename-map.js +67 -0
- package/src/templates/fuse-packs/access-control-tools/inject-qa-ids.js +14 -0
- package/src/templates/fuse-packs/access-control-tools/lib/hierarchy-scanner.js +628 -0
- package/src/templates/fuse-packs/access-control-tools/lib/hierarchy-scanner.test.js +323 -0
- package/src/templates/fuse-packs/access-control-tools/lib/html-scanner.js +362 -0
- package/src/templates/fuse-packs/access-control-tools/lib/id-rules.duplicate.test.js +136 -0
- package/src/templates/fuse-packs/access-control-tools/lib/id-rules.js +1095 -0
- package/src/templates/fuse-packs/access-control-tools/lib/ts-component-index.js +482 -0
- package/src/templates/fuse-packs/access-control-tools/output/.gitkeep +0 -0
- package/src/templates/fuse-packs/access-control-tools/run.sh +223 -0
- package/src/templates/fuse-packs/access-control-tools/scan-access-control.js +14 -0
- package/src/templates/fuse-packs/iam-assets/sound/apple-play.mp3 +0 -0
- package/src/templates/fuse-packs/iam-assets/sound/remove.mp3 +0 -0
- package/src/templates/generator/README.md +12 -0
- package/src/templates/testing/ci.cjs +45 -0
- package/src/templates/testing/mock.cjs +149 -0
- package/src/templates/testing/playwright.config.cjs +25 -0
- package/src/templates/testing/serve.cjs +22 -0
- package/src/templates/testing/smoke.spec.cjs +180 -0
- package/src/utils/branding.js +39 -0
- package/src/utils/colors.js +18 -0
- package/src/utils/diff.js +109 -0
- package/src/utils/merger.js +115 -0
- package/src/validator/backend-diagnostics.js +30 -0
- package/src/validator/blueprint-validator.js +276 -0
- package/src/validator/diagnostics.js +608 -0
- package/src/validator/pattern.js +28 -0
- package/test/auth-interceptor.js +84 -0
- package/test/auth-provision-checks.js +297 -0
- package/test/field-preferences.js +145 -0
- package/test/fixtures/field-profile.gdl +7 -0
- package/test/fixtures/mock.jdl +17 -0
- package/test/fixtures/mock.png +0 -0
- package/test/fixtures/performance/config.yml +33 -0
- package/test/fixtures/performance/large.gdl +199 -0
- package/test/fixtures/performance/small.gdl +7 -0
- package/test/fixtures/quality/custom.yml +22 -0
- package/test/fixtures/quality/go-duck.yml +22 -0
- package/test/fixtures/quality/jhipster.yml +30 -0
- package/test/fixtures/quality/representative.gdl +14 -0
- package/test/list-state.js +138 -0
- package/test/micro-functions-layout.js +95 -0
- package/test/performance-generation.js +32 -0
- package/test/pmb-preset.js +127 -0
- package/test/project-workflow.js +111 -0
- package/test/quality-features.js +250 -0
- package/test/verify-commands.js +83 -0
- package/test/verify.js +959 -0
- package/test-files/GDL/example.gdl +15 -0
- package/test-files/JDL/example.jdl +15 -0
- package/test-files/config/crudforge.config.yml +127 -0
- package/test-run.sh +272 -0
|
@@ -0,0 +1,965 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const ejs = require('ejs');
|
|
4
|
+
const { deriveAuthUrls, stripSecretsForGenerate, validateAuthConfig } = require('../config/auth-config');
|
|
5
|
+
|
|
6
|
+
function isKeycloakAuthEnabled(config) {
|
|
7
|
+
return config.auth?.provider === 'keycloak';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Live login forms never inherit provisioning credentials from the CLI environment.
|
|
12
|
+
*/
|
|
13
|
+
function resolveSignInPrefill(auth = {}) {
|
|
14
|
+
const configured = auth.signInPrefill && typeof auth.signInPrefill === 'object' ? auth.signInPrefill : {};
|
|
15
|
+
if (auth.useMockAuth) {
|
|
16
|
+
return {
|
|
17
|
+
username: configured.username || 'demo@crudforge.com',
|
|
18
|
+
password: configured.password || 'admin',
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
username: configured.username || '',
|
|
23
|
+
password: configured.password || '',
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resolveTokenUrl(auth = {}) {
|
|
28
|
+
const derived = deriveAuthUrls({ ...auth });
|
|
29
|
+
if (derived.tokenUrl) {
|
|
30
|
+
return derived.tokenUrl;
|
|
31
|
+
}
|
|
32
|
+
return '/auth/realms/crudforge/protocol/openid-connect/token';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolveBrowserTokenUrl(auth = {}) {
|
|
36
|
+
const realm = auth.realm || 'crudforge';
|
|
37
|
+
if (!auth.useMockAuth && auth.serverUrl) {
|
|
38
|
+
return `/realms/${realm}/protocol/openid-connect/token`;
|
|
39
|
+
}
|
|
40
|
+
return resolveTokenUrl(auth);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function applyAuthPack({ outputDir, config, writeOrPreview, folderName, entities = [] }) {
|
|
44
|
+
if (!isKeycloakAuthEnabled(config) || !config.fuse?.enabled) {
|
|
45
|
+
return { generated: 0 };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const derived = deriveAuthUrls({ ...(config.auth || {}) });
|
|
49
|
+
const authForGenerate = {
|
|
50
|
+
...derived,
|
|
51
|
+
provision: { ...(derived.provision || {}), enabled: false },
|
|
52
|
+
};
|
|
53
|
+
validateAuthConfig({ ...config, auth: authForGenerate }, { requireProvision: false });
|
|
54
|
+
|
|
55
|
+
const templatesDir = path.join(__dirname, '..', 'templates', 'angular');
|
|
56
|
+
const auth = {
|
|
57
|
+
...stripSecretsForGenerate(derived),
|
|
58
|
+
provider: 'keycloak',
|
|
59
|
+
tokenUrl: resolveBrowserTokenUrl(derived),
|
|
60
|
+
signInPrefill: resolveSignInPrefill(derived),
|
|
61
|
+
};
|
|
62
|
+
const iam = { enabled: true, ...config.iam };
|
|
63
|
+
const locals = {
|
|
64
|
+
auth,
|
|
65
|
+
iam,
|
|
66
|
+
project: config.project || {},
|
|
67
|
+
config,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const authLayout = [
|
|
71
|
+
{ template: 'current-user.ts', out: 'utils/current-user.ts' },
|
|
72
|
+
{ template: 'auth.utils.ts', out: 'utils/auth.utils.ts' },
|
|
73
|
+
{ template: 'permission-attribute.util.ts', out: 'utils/permission-attribute.util.ts' },
|
|
74
|
+
{ template: 'auth.service.ts', out: 'services/auth.service.ts' },
|
|
75
|
+
{ template: 'access-control.service.ts', out: 'services/access-control.service.ts' },
|
|
76
|
+
{ template: 'keycloak-admin.service.ts', out: 'services/keycloak-admin.service.ts' },
|
|
77
|
+
{ template: 'auth.interceptor.ts', out: 'interceptors/auth.interceptor.ts' },
|
|
78
|
+
{ template: 'auth.guard.ts', out: 'guards/auth.guard.ts' },
|
|
79
|
+
{ template: 'no-auth.guard.ts', out: 'guards/no-auth.guard.ts' },
|
|
80
|
+
{ template: 'landing.guard.ts', out: 'guards/landing.guard.ts' },
|
|
81
|
+
{ template: 'access-ctrl.pipe.ts', out: 'pipes/access-ctrl.pipe.ts' },
|
|
82
|
+
{ template: 'has-permission.directive.ts', out: 'directives/has-permission.directive.ts' },
|
|
83
|
+
{ template: 'index.ts', out: 'index.ts' },
|
|
84
|
+
{
|
|
85
|
+
template: 'disabled-account-overlay.component.ts',
|
|
86
|
+
out: 'components/disabled-account-overlay/disabled-account-overlay.component.ts',
|
|
87
|
+
html: 'disabled-account-overlay.component.html',
|
|
88
|
+
css: 'disabled-account-overlay.component.css',
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
let generated = 0;
|
|
93
|
+
const authOutDir = path.join(outputDir, 'src/app/core/auth');
|
|
94
|
+
for (const entry of authLayout) {
|
|
95
|
+
const rendered = await renderTemplate(path.join(templatesDir, 'auth', `${entry.template}.ejs`), locals);
|
|
96
|
+
writeOrPreview(path.join(authOutDir, entry.out), rendered);
|
|
97
|
+
generated += 1;
|
|
98
|
+
if (entry.html) {
|
|
99
|
+
const html = await renderTemplate(path.join(templatesDir, 'auth', `${entry.html}.ejs`), locals);
|
|
100
|
+
writeOrPreview(path.join(authOutDir, path.dirname(entry.out), entry.html), html);
|
|
101
|
+
generated += 1;
|
|
102
|
+
}
|
|
103
|
+
if (entry.css) {
|
|
104
|
+
const css = await renderTemplate(path.join(templatesDir, 'auth', `${entry.css}.ejs`), locals);
|
|
105
|
+
writeOrPreview(path.join(authOutDir, path.dirname(entry.out), entry.css), css);
|
|
106
|
+
generated += 1;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
writeOrPreview(path.join(authOutDir, 'AGENT_README.md'), renderAuthAgentReadme(locals));
|
|
110
|
+
generated += 1;
|
|
111
|
+
removeLegacyFlatAuthFiles(authOutDir);
|
|
112
|
+
|
|
113
|
+
const signInTs = await renderTemplate(path.join(templatesDir, 'auth', 'sign-in.ts.ejs'), locals);
|
|
114
|
+
writeOrPreview(path.join(outputDir, 'src/app/domains/auth/features/sign-in/sign-in.ts'), signInTs);
|
|
115
|
+
generated += 1;
|
|
116
|
+
|
|
117
|
+
patchAppConfig(outputDir, writeOrPreview);
|
|
118
|
+
patchAppRootOverlay(outputDir, writeOrPreview);
|
|
119
|
+
patchAdminRoutesGuard(outputDir, config, writeOrPreview);
|
|
120
|
+
patchLandingAndNoAccess(outputDir, config, writeOrPreview);
|
|
121
|
+
patchAuthRoutesGuard(outputDir, writeOrPreview);
|
|
122
|
+
patchSignInHtml(outputDir, writeOrPreview);
|
|
123
|
+
patchNavigationFilter(outputDir, writeOrPreview);
|
|
124
|
+
writeOrPreview(path.join(outputDir, 'src/app/domains/admin/layout/ui/user.ts'), await renderTemplate(path.join(templatesDir, 'auth', 'fuse-user.ts.ejs'), locals));
|
|
125
|
+
|
|
126
|
+
const accountFile = path.join(outputDir, 'src/app/domains/admin/modules/extras/settings/features/account.ts');
|
|
127
|
+
if (fs.existsSync(accountFile)) writeOrPreview(accountFile, await renderTemplate(path.join(templatesDir, 'auth', 'fuse-account.ts.ejs'), locals));
|
|
128
|
+
|
|
129
|
+
const noAccess = await renderTemplate(path.join(templatesDir, 'iam', 'no-access.component.ts.ejs'), locals);
|
|
130
|
+
writeOrPreview(
|
|
131
|
+
path.join(outputDir, 'src/app/domains/admin/modules/extras/no-access/no-access.component.ts'),
|
|
132
|
+
noAccess
|
|
133
|
+
);
|
|
134
|
+
generated += 1;
|
|
135
|
+
|
|
136
|
+
if (iam.enabled !== false && iam.users !== false) {
|
|
137
|
+
generated += await generateIamModule({
|
|
138
|
+
outputDir,
|
|
139
|
+
templatesDir,
|
|
140
|
+
locals,
|
|
141
|
+
writeOrPreview,
|
|
142
|
+
folderName,
|
|
143
|
+
config,
|
|
144
|
+
entities,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { generated };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function removeLegacyFlatAuthFiles(authOutDir) {
|
|
152
|
+
// Older generates wrote auth files flat under core/auth/. Keep only the barrel + docs at root.
|
|
153
|
+
const keep = new Set(['index.ts', 'AGENT_README.md']);
|
|
154
|
+
if (!fs.existsSync(authOutDir)) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
for (const entry of fs.readdirSync(authOutDir)) {
|
|
158
|
+
if (keep.has(entry)) continue;
|
|
159
|
+
const full = path.join(authOutDir, entry);
|
|
160
|
+
const stat = fs.statSync(full);
|
|
161
|
+
if (stat.isFile() && /\.(ts|js|map)$/.test(entry)) {
|
|
162
|
+
fs.removeSync(full);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function renderAuthAgentReadme(locals = {}) {
|
|
168
|
+
const auth = locals.auth || {};
|
|
169
|
+
const projectName = locals.project?.name || 'CRUDForge App';
|
|
170
|
+
return `# Auth (Keycloak) — agent guide
|
|
171
|
+
|
|
172
|
+
> **Generated by \`@crudforge/cli\`** (\`crudforge generate\`) for **${projectName}**.
|
|
173
|
+
> Location: \`src/app/core/auth/\` (alongside \`src/app/core/user-management/\`).
|
|
174
|
+
|
|
175
|
+
## Purpose
|
|
176
|
+
|
|
177
|
+
Browser-side Keycloak authentication, HTTP bearer attachment, route guards, and ACL helpers.
|
|
178
|
+
Do **not** put admin passwords or client secrets here. Provisioning stays in the CRUDForge CLI (\`bootstrap\`).
|
|
179
|
+
|
|
180
|
+
## Layout
|
|
181
|
+
|
|
182
|
+
\`\`\`
|
|
183
|
+
src/app/core/auth/
|
|
184
|
+
AGENT_README.md
|
|
185
|
+
index.ts ← public barrel (\`@/app/core/auth\`)
|
|
186
|
+
services/
|
|
187
|
+
auth.service.ts ← sign-in / refresh / sign-out / token storage
|
|
188
|
+
access-control.service.ts ← screen + component ACL from JWT + Keycloak attrs
|
|
189
|
+
keycloak-admin.service.ts ← Admin REST (users/roles) via Fuse proxy
|
|
190
|
+
guards/
|
|
191
|
+
auth.guard.ts ← require authenticated session
|
|
192
|
+
no-auth.guard.ts ← sign-in only when logged out
|
|
193
|
+
landing.guard.ts ← post-login landing / no-access routing
|
|
194
|
+
interceptors/
|
|
195
|
+
auth.interceptor.ts ← Authorization + X-Forwarded-Proto + refresh retry
|
|
196
|
+
pipes/
|
|
197
|
+
access-ctrl.pipe.ts ← \`*ngIf="SCREEN | accessCtrl: 'edit'"\`
|
|
198
|
+
directives/
|
|
199
|
+
has-permission.directive.ts
|
|
200
|
+
utils/
|
|
201
|
+
auth.utils.ts ← JWT helpers
|
|
202
|
+
permission-attribute.util.ts ← screen attribute normalize/merge
|
|
203
|
+
components/
|
|
204
|
+
disabled-account-overlay/ ← countdown lock UI (auth.disabledAccount.*)
|
|
205
|
+
\`\`\`
|
|
206
|
+
|
|
207
|
+
Sign-in UI lives under \`src/app/domains/auth/features/sign-in/\` (imports \`AuthService\` from this pack).
|
|
208
|
+
|
|
209
|
+
## How it works
|
|
210
|
+
|
|
211
|
+
1. **Sign-in** — password grant against \`environment.sso\` (proxied \`/realms/...\` when live).
|
|
212
|
+
2. **Session** — access/refresh tokens in namespaced storage (\`cforge:{realm}:{clientId}\`). Default idle timeout is \`0\` (stay signed in until logout). Resilient refresh keeps the session on network/5xx; only terminal auth errors clear tokens. Optional focus refresh + cross-tab sync via \`auth.session.*\`.
|
|
213
|
+
3. **HTTP** — \`authInterceptor\` attaches Bearer tokens; refreshes once on 401 when not mock; may start the disabled-account overlay instead of an immediate sign-out.
|
|
214
|
+
4. **ACL** — \`AccessControlService\` combines realm/client roles with user/role screen attributes for nav + \`accessCtrl\` stamps.
|
|
215
|
+
5. **Admin API** — \`KeycloakAdminService\` calls \`/admin/realms/{realm}\` (or mock data when \`useMockAuth\`).
|
|
216
|
+
6. **Disabled account** — when \`auth.disabledAccount.enabled\`, explicit Keycloak “account disabled” grant errors freeze the shell (\`app.ts\` overlay) then sign out after the countdown.
|
|
217
|
+
|
|
218
|
+
## Current snapshot (safe fields)
|
|
219
|
+
|
|
220
|
+
- provider: \`${auth.provider || 'keycloak'}\`
|
|
221
|
+
- realm: \`${auth.realm || ''}\`
|
|
222
|
+
- clientId: \`${auth.clientId || ''}\`
|
|
223
|
+
- useMockAuth: \`${auth.useMockAuth ? 'true' : 'false'}\`
|
|
224
|
+
|
|
225
|
+
## Agent rules
|
|
226
|
+
|
|
227
|
+
- Prefer importing from \`@/app/core/auth\` (barrel), not deep file paths, unless you need a non-exported helper.
|
|
228
|
+
- Edit CRUDForge templates under \`src/templates/angular/auth/\`, then regenerate — do not treat the testing-area copy as source of truth.
|
|
229
|
+
- Never emit \`clientSecret\` or provision passwords into Angular environments.
|
|
230
|
+
- Keep User Management UI in \`src/app/core/user-management/\`; this pack is the shared auth/ACL substrate.
|
|
231
|
+
|
|
232
|
+
## Related
|
|
233
|
+
|
|
234
|
+
- \`src/app/core/user-management/AGENT_README.md\`
|
|
235
|
+
- App \`AGENT_README.md\` / \`docs/ai/ARCHITECTURE.md\`
|
|
236
|
+
- Generator \`QA/KEYCLOAK-BOOTSTRAP.md\`
|
|
237
|
+
`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function csvEscape(value) {
|
|
241
|
+
const text = String(value == null ? '' : value);
|
|
242
|
+
if (/[",\n\r]/.test(text)) {
|
|
243
|
+
return `"${text.replace(/"/g, '""')}"`;
|
|
244
|
+
}
|
|
245
|
+
return text;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function buildScreenAccessCatalog({ entities = [], folderName = 'micro-functions', iam = {} }) {
|
|
249
|
+
const rows = [];
|
|
250
|
+
const screens = [];
|
|
251
|
+
const pushRow = (screenName, screenId, componentName, componentId, componentType, description) => {
|
|
252
|
+
rows.push({ screenName, screenId, componentName, componentId, componentType, description });
|
|
253
|
+
};
|
|
254
|
+
const pushScreen = (screenName, screenId, buttons) => {
|
|
255
|
+
screens.push({ id: screenId, label: screenName });
|
|
256
|
+
pushRow(screenName, screenId, 'Screen Access', screenId, 'SCREEN', `${screenName} — screen access.`);
|
|
257
|
+
for (const button of buttons) {
|
|
258
|
+
pushRow(
|
|
259
|
+
screenName,
|
|
260
|
+
screenId,
|
|
261
|
+
button.name,
|
|
262
|
+
button.id,
|
|
263
|
+
'BUTTON',
|
|
264
|
+
button.description || `Allows the "${button.name}" action on the ${screenName} screen.`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// Semantic componentIds (aligned with access-control-tools/lib/id-rules.js).
|
|
270
|
+
const entityButtons = [
|
|
271
|
+
{ id: 'view', name: 'View' },
|
|
272
|
+
{ id: 'edit', name: 'Edit' },
|
|
273
|
+
{ id: 'delete', name: 'Delete' },
|
|
274
|
+
{ id: 'create', name: 'Create' },
|
|
275
|
+
{ id: 'open-filter', name: 'Filter' },
|
|
276
|
+
{ id: 'toggle-view', name: 'Toggle View' },
|
|
277
|
+
{ id: 'open-columns', name: 'Columns' },
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
for (const entity of entities) {
|
|
281
|
+
const kebab = String(entity.name || '')
|
|
282
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
283
|
+
.replace(/[\s_]+/g, '-')
|
|
284
|
+
.toLowerCase();
|
|
285
|
+
const entityFolder = entity.folderName || folderName;
|
|
286
|
+
const screenId = `${entityFolder}/${kebab}`;
|
|
287
|
+
const buttons = [...entityButtons];
|
|
288
|
+
if ((entity.fields || []).some((field) => field.fieldName === 'isActive')) {
|
|
289
|
+
buttons.push({ id: 'toggle-active', name: 'Toggle Active' });
|
|
290
|
+
}
|
|
291
|
+
pushScreen(entity.name, screenId, buttons);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (iam.users !== false) {
|
|
295
|
+
pushScreen('User Management', 'user-management', [
|
|
296
|
+
{ id: 'create', name: 'Create' },
|
|
297
|
+
{ id: 'view', name: 'View' },
|
|
298
|
+
{ id: 'edit', name: 'Edit' },
|
|
299
|
+
{ id: 'delete', name: 'Delete' },
|
|
300
|
+
{ id: 'change-password', name: 'Change Password' },
|
|
301
|
+
{ id: 'access', name: 'Assign Role Matrix' },
|
|
302
|
+
{ id: 'permissions', name: 'Assign User Matrix' },
|
|
303
|
+
{ id: 'open-columns', name: 'Columns' },
|
|
304
|
+
{ id: 'toggle-view', name: 'Toggle View' },
|
|
305
|
+
{ id: 'copy-username', name: 'Copy Username' },
|
|
306
|
+
{ id: 'copy-email', name: 'Copy Email' },
|
|
307
|
+
{ id: 'toggle-enabled', name: 'Toggle Enabled' },
|
|
308
|
+
{ id: 'roles', name: 'Roles' },
|
|
309
|
+
]);
|
|
310
|
+
}
|
|
311
|
+
if (iam.roles !== false) {
|
|
312
|
+
pushScreen('Role Management', 'role-management', [
|
|
313
|
+
{ id: 'toggle-create-form', name: 'Create Role' },
|
|
314
|
+
{ id: 'toggle-create-form-2', name: 'Cancel Create' },
|
|
315
|
+
{ id: 'submit', name: 'Submit Create' },
|
|
316
|
+
{ id: 'sync-holders', name: 'Sync Holders' },
|
|
317
|
+
{ id: 'role-permissions', name: 'Permission Matrix' },
|
|
318
|
+
{ id: 'select-role', name: 'Select Role' },
|
|
319
|
+
{ id: 'search-clear', name: 'Clear Role Search' },
|
|
320
|
+
{ id: 'go-back-to-list', name: 'Back to Roles' },
|
|
321
|
+
]);
|
|
322
|
+
pushScreen('User Select Dialog', 'user-select-dialog', [
|
|
323
|
+
{ id: 'on-close', name: 'Close' },
|
|
324
|
+
{ id: 'clear-search', name: 'Clear Search' },
|
|
325
|
+
{ id: 'assign-user', name: 'Assign User' },
|
|
326
|
+
]);
|
|
327
|
+
}
|
|
328
|
+
if (iam.rolePermissions !== false) {
|
|
329
|
+
pushScreen('Role Permissions', 'role-permissions', [{ id: 'toggle', name: 'Toggle Grant' }]);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
for (const extra of iam.screens || []) {
|
|
333
|
+
if (!extra || screens.some((screen) => screen.id === extra)) continue;
|
|
334
|
+
pushScreen(String(extra).split('/').pop()?.replace(/-/g, ' ') || extra, String(extra), []);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const header = 'screenName,screenId,componentName,componentId,componentType,description';
|
|
338
|
+
const csv = [
|
|
339
|
+
header,
|
|
340
|
+
...rows.map((row) =>
|
|
341
|
+
[row.screenName, row.screenId, row.componentName, row.componentId, row.componentType, row.description]
|
|
342
|
+
.map(csvEscape)
|
|
343
|
+
.join(',')
|
|
344
|
+
),
|
|
345
|
+
].join('\n');
|
|
346
|
+
|
|
347
|
+
return { csv: `${csv}\n`, screens, rows };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function generateIamModule({ outputDir, templatesDir, locals, writeOrPreview, folderName, config, entities = [] }) {
|
|
351
|
+
const iamRoot = path.join(outputDir, 'src/app/core/user-management');
|
|
352
|
+
const catalog = buildScreenAccessCatalog({
|
|
353
|
+
entities,
|
|
354
|
+
folderName: folderName || 'micro-functions',
|
|
355
|
+
iam: locals.iam || {},
|
|
356
|
+
});
|
|
357
|
+
locals = { ...locals, managedScreens: catalog.screens };
|
|
358
|
+
|
|
359
|
+
// PMB-style ACL registry — matrices load this CSV at runtime.
|
|
360
|
+
writeOrPreview(path.join(outputDir, 'public/assets/screen-components.csv'), catalog.csv);
|
|
361
|
+
|
|
362
|
+
/** Flat template name → foldered output under src/app/core/user-management */
|
|
363
|
+
const iamLayout = [
|
|
364
|
+
{ template: 'user-management.routes.ts', out: 'user-management.routes.ts' },
|
|
365
|
+
{ template: 'screen-catalog.ts', out: 'shared/screen-catalog.ts' },
|
|
366
|
+
{
|
|
367
|
+
template: 'user-list.component.ts',
|
|
368
|
+
out: 'users/user-list/user-list.component.ts',
|
|
369
|
+
html: 'user-list.component.html',
|
|
370
|
+
scss: 'user-list.component.scss',
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
template: 'user-form.component.ts',
|
|
374
|
+
out: 'users/user-form/user-form.component.ts',
|
|
375
|
+
html: 'user-form.component.html',
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
template: 'user-detail.component.ts',
|
|
379
|
+
out: 'users/user-detail/user-detail.component.ts',
|
|
380
|
+
html: 'user-detail.component.html',
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
template: 'change-password.component.ts',
|
|
384
|
+
out: 'users/change-password/change-password.component.ts',
|
|
385
|
+
html: 'change-password.component.html',
|
|
386
|
+
},
|
|
387
|
+
];
|
|
388
|
+
if (locals.iam.roles !== false) {
|
|
389
|
+
iamLayout.push({
|
|
390
|
+
template: 'role-list.component.ts',
|
|
391
|
+
out: 'roles/role-list/role-list.component.ts',
|
|
392
|
+
html: 'role-list.component.html',
|
|
393
|
+
scss: 'role-list.component.scss',
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
if (locals.iam.rolePermissions !== false) {
|
|
397
|
+
iamLayout.push({
|
|
398
|
+
template: 'role-permission-matrix.component.ts',
|
|
399
|
+
out: 'matrices/role-permission-matrix/role-permission-matrix.component.ts',
|
|
400
|
+
html: 'role-permission-matrix.component.html',
|
|
401
|
+
scss: 'role-permission-matrix.component.scss',
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
if (locals.iam.userAccess !== false) {
|
|
405
|
+
iamLayout.push({
|
|
406
|
+
template: 'user-access-matrix.component.ts',
|
|
407
|
+
out: 'matrices/user-access-matrix/user-access-matrix.component.ts',
|
|
408
|
+
html: 'user-access-matrix.component.html',
|
|
409
|
+
scss: 'user-access-matrix.component.scss',
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
if (locals.iam.userPermissions !== false) {
|
|
413
|
+
iamLayout.push({
|
|
414
|
+
template: 'user-permission-matrix.component.ts',
|
|
415
|
+
out: 'matrices/user-permission-matrix/user-permission-matrix.component.ts',
|
|
416
|
+
html: 'user-permission-matrix.component.html',
|
|
417
|
+
scss: 'user-permission-matrix.component.scss',
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
let generatedFiles = 0;
|
|
422
|
+
|
|
423
|
+
// Shared matrix helpers (PMB-aligned CSV clustering, sounds, highlight, skeleton).
|
|
424
|
+
for (const shared of [
|
|
425
|
+
'permission-matrix-sound.ts',
|
|
426
|
+
'screen-matrix.util.ts',
|
|
427
|
+
'role-display.util.ts',
|
|
428
|
+
'text-highlight.pipe.ts',
|
|
429
|
+
'role-permission-skeleton.component.ts',
|
|
430
|
+
'confirm-dialog.component.ts',
|
|
431
|
+
'screen-navigation-map.ts',
|
|
432
|
+
'screen-navigation.registry.ts',
|
|
433
|
+
'matrix-hierarchy.util.ts',
|
|
434
|
+
]) {
|
|
435
|
+
const rendered = await renderTemplate(path.join(templatesDir, 'iam', 'shared', `${shared}.ejs`), locals);
|
|
436
|
+
writeOrPreview(path.join(iamRoot, 'shared', shared), rendered);
|
|
437
|
+
generatedFiles += 1;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Empty hierarchy until access-control sync generates screen-navigation-map.json
|
|
441
|
+
{
|
|
442
|
+
const navAsset = path.join(outputDir, 'public/assets/screen-navigation-map.json');
|
|
443
|
+
const emptyNav =
|
|
444
|
+
JSON.stringify(
|
|
445
|
+
{
|
|
446
|
+
version: 1,
|
|
447
|
+
generatedAt: null,
|
|
448
|
+
pages: [],
|
|
449
|
+
unresolved: [],
|
|
450
|
+
stats: { pageCount: 0, embedEdgeCount: 0, unresolvedCount: 0 },
|
|
451
|
+
},
|
|
452
|
+
null,
|
|
453
|
+
2
|
|
454
|
+
) + '\n';
|
|
455
|
+
writeOrPreview(navAsset, emptyNav);
|
|
456
|
+
generatedFiles += 1;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Role Management subcomponents (PMB role-management layout).
|
|
460
|
+
if (locals.iam.roles !== false) {
|
|
461
|
+
const roleMgmtParts = [
|
|
462
|
+
{
|
|
463
|
+
base: 'role-header',
|
|
464
|
+
outDir: 'roles/role-list/components/role-header',
|
|
465
|
+
files: ['ts', 'html'],
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
base: 'role-users',
|
|
469
|
+
outDir: 'roles/role-list/components/role-users',
|
|
470
|
+
files: ['ts', 'html'],
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
base: 'role-inheritance',
|
|
474
|
+
outDir: 'roles/role-list/components/role-inheritance',
|
|
475
|
+
files: ['ts', 'html'],
|
|
476
|
+
when: () => locals.iam.inheritance !== false,
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
base: 'role-permissions-panel',
|
|
480
|
+
outDir: 'roles/role-list/components/role-permissions-panel',
|
|
481
|
+
files: ['ts', 'html', 'css'],
|
|
482
|
+
when: () => locals.iam.rolePermissions !== false,
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
base: 'user-select-dialog',
|
|
486
|
+
outDir: 'roles/role-list/components/user-select-dialog',
|
|
487
|
+
files: ['ts', 'html'],
|
|
488
|
+
},
|
|
489
|
+
];
|
|
490
|
+
for (const part of roleMgmtParts) {
|
|
491
|
+
if (part.when && !part.when()) continue;
|
|
492
|
+
for (const ext of part.files) {
|
|
493
|
+
const templateName =
|
|
494
|
+
ext === 'ts'
|
|
495
|
+
? `${part.base}.component.ts.ejs`
|
|
496
|
+
: ext === 'html'
|
|
497
|
+
? `${part.base}.component.html.ejs`
|
|
498
|
+
: `${part.base}.component.css.ejs`;
|
|
499
|
+
const outName =
|
|
500
|
+
ext === 'ts'
|
|
501
|
+
? `${part.base}.component.ts`
|
|
502
|
+
: ext === 'html'
|
|
503
|
+
? `${part.base}.component.html`
|
|
504
|
+
: `${part.base}.component.css`;
|
|
505
|
+
const rendered = await renderTemplate(
|
|
506
|
+
path.join(templatesDir, 'iam', 'role-management', templateName),
|
|
507
|
+
locals
|
|
508
|
+
);
|
|
509
|
+
writeOrPreview(path.join(iamRoot, part.outDir, outName), rendered);
|
|
510
|
+
generatedFiles += 1;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Ship PMB-style toggle sounds into public assets.
|
|
516
|
+
const soundSrcDir = path.join(__dirname, '..', 'templates', 'fuse-packs', 'iam-assets', 'sound');
|
|
517
|
+
const soundDestDir = path.join(outputDir, 'public', 'assets', 'sound');
|
|
518
|
+
if (fs.existsSync(soundSrcDir)) {
|
|
519
|
+
for (const file of fs.readdirSync(soundSrcDir)) {
|
|
520
|
+
const from = path.join(soundSrcDir, file);
|
|
521
|
+
if (!fs.statSync(from).isFile()) continue;
|
|
522
|
+
const to = path.join(soundDestDir, file);
|
|
523
|
+
if (config.dryRun) {
|
|
524
|
+
writeOrPreview(to, `[binary:${file}]`);
|
|
525
|
+
} else {
|
|
526
|
+
fs.ensureDirSync(soundDestDir);
|
|
527
|
+
fs.copyFileSync(from, to);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
for (const entry of iamLayout) {
|
|
533
|
+
const rendered = await renderTemplate(path.join(templatesDir, 'iam', `${entry.template}.ejs`), locals);
|
|
534
|
+
writeOrPreview(path.join(iamRoot, entry.out), rendered);
|
|
535
|
+
generatedFiles += 1;
|
|
536
|
+
if (entry.html) {
|
|
537
|
+
const htmlRendered = await renderTemplate(path.join(templatesDir, 'iam', `${entry.html}.ejs`), locals);
|
|
538
|
+
const htmlOut = path.join(path.dirname(entry.out), entry.html);
|
|
539
|
+
writeOrPreview(path.join(iamRoot, htmlOut), htmlRendered);
|
|
540
|
+
generatedFiles += 1;
|
|
541
|
+
}
|
|
542
|
+
if (entry.scss) {
|
|
543
|
+
const scssRendered = await renderTemplate(path.join(templatesDir, 'iam', `${entry.scss}.ejs`), locals);
|
|
544
|
+
const scssOut = path.join(path.dirname(entry.out), entry.scss);
|
|
545
|
+
writeOrPreview(path.join(iamRoot, scssOut), scssRendered);
|
|
546
|
+
generatedFiles += 1;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
writeOrPreview(path.join(iamRoot, 'AGENT_README.md'), renderUserManagementAgentReadme(locals));
|
|
551
|
+
generatedFiles += 2; // CSV + AGENT_README
|
|
552
|
+
|
|
553
|
+
registerIamRoutes(outputDir, config, folderName, writeOrPreview);
|
|
554
|
+
registerIamNavigation(outputDir, config, folderName, writeOrPreview);
|
|
555
|
+
return generatedFiles;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function renderUserManagementAgentReadme(locals = {}) {
|
|
559
|
+
const iam = locals.iam || {};
|
|
560
|
+
const auth = locals.auth || {};
|
|
561
|
+
const projectName = locals.project?.name || 'CRUDForge App';
|
|
562
|
+
const features = [
|
|
563
|
+
'- **Users** (`users/user-list`) — list / search / create / edit / view / delete Keycloak users; drawer + page modes',
|
|
564
|
+
'- **User form** (`users/user-form`) — create/edit identity fields via `KeycloakAdminService`',
|
|
565
|
+
'- **User detail** (`users/user-detail`) — read-only profile with links to role/screen matrices',
|
|
566
|
+
'- **Change password** (`users/change-password`) — dialog to set/reset password (temporary flag supported)',
|
|
567
|
+
];
|
|
568
|
+
if (iam.roles !== false) {
|
|
569
|
+
features.push(
|
|
570
|
+
'- **Roles** (`roles/role-list`) — PMB Role Management: master-detail list, create, duplicate, assign users, sync holders, screen-permission summary'
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
if (iam.rolePermissions !== false) {
|
|
574
|
+
features.push(
|
|
575
|
+
'- **Role permissions** (`matrices/role-permission-matrix`) — map roles → screen IDs; UI locks until role write + holder sync; grant/revoke sounds'
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
if (iam.userAccess !== false) {
|
|
579
|
+
features.push(
|
|
580
|
+
'- **User access** (`matrices/user-access-matrix`) — assign realm roles to a user (lock + sounds while syncing)'
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
if (iam.userPermissions !== false) {
|
|
584
|
+
features.push(
|
|
585
|
+
'- **User permissions** (`matrices/user-permission-matrix`) — map a user → screen IDs (lock + sounds while saving)'
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return `# User Management (IAM) — agent guide
|
|
590
|
+
|
|
591
|
+
> **Generated by \`@crudforge/cli\`** (\`crudforge generate\`) for **${projectName}**.
|
|
592
|
+
> Location: \`src/app/core/user-management/\` (not under \`domains/admin/modules/apps\`).
|
|
593
|
+
|
|
594
|
+
## Purpose
|
|
595
|
+
|
|
596
|
+
Angular overlay for Keycloak identity administration. It is **not** a JDL entity.
|
|
597
|
+
Do not model User/Role in the domain JDL; keep IAM here and behind \`auth.provider: keycloak\` + \`iam.enabled\`.
|
|
598
|
+
|
|
599
|
+
## Layout
|
|
600
|
+
|
|
601
|
+
\`\`\`
|
|
602
|
+
src/app/core/user-management/
|
|
603
|
+
AGENT_README.md
|
|
604
|
+
user-management.routes.ts
|
|
605
|
+
shared/screen-catalog.ts
|
|
606
|
+
users/{user-list,user-form,user-detail,change-password}/
|
|
607
|
+
*.component.ts + *.component.html ← Angular-style split templates
|
|
608
|
+
roles/role-list/
|
|
609
|
+
role-list.component.{ts,html,scss}
|
|
610
|
+
components/{role-header,role-users,role-inheritance,role-permissions-panel,user-select-dialog}/
|
|
611
|
+
matrices/{role-permission-matrix,user-access-matrix,user-permission-matrix}/
|
|
612
|
+
shared/{permission-matrix-sound,screen-matrix.util,confirm-dialog,role-display,…}
|
|
613
|
+
public/assets/sound/{apple-play,remove}.mp3
|
|
614
|
+
\`\`\`
|
|
615
|
+
|
|
616
|
+
Each screen is a standalone Angular component (\`templateUrl\`, not inline \`template\`).
|
|
617
|
+
|
|
618
|
+
Auth primitives stay next door in \`src/app/core/auth/\` (see that folder's \`AGENT_README.md\` — services, guards, interceptors, pipes).
|
|
619
|
+
|
|
620
|
+
## Features enabled in this generate
|
|
621
|
+
|
|
622
|
+
${features.join('\n')}
|
|
623
|
+
|
|
624
|
+
## How it works
|
|
625
|
+
|
|
626
|
+
1. **Routing** — Admin shell loads \`@/app/core/user-management/user-management.routes\` under path \`user-management\`. Public URLs remain \`/admin/user-management/**\`.
|
|
627
|
+
2. **API** — UI calls \`KeycloakAdminService\`, which talks to Keycloak Admin REST through the Fuse proxy (\`/admin/realms\`, \`/realms\`). Browser never embeds admin passwords or client secrets.
|
|
628
|
+
3. **ACL** — Buttons/screens use \`accessCtrl\` pipe + \`data-screen-id="user-management"\` (and related screen IDs). \`AccessControlService\` filters nav and enforces admin roles from \`environment.adminRoles\`.
|
|
629
|
+
4. **Screen catalog** — \`shared/screen-catalog.ts\` merges nav IDs with IAM screens so permission matrices stay aligned with the sidebar.
|
|
630
|
+
5. **Mock vs live** — \`environment.useMockAuth\` drives mock Keycloak data for Playwright; live mode uses \`auth.serverUrl\` / proxied Admin REST.
|
|
631
|
+
|
|
632
|
+
## Current auth snapshot (safe fields only)
|
|
633
|
+
|
|
634
|
+
- provider: \`${auth.provider || 'keycloak'}\`
|
|
635
|
+
- realm: \`${auth.realm || ''}\`
|
|
636
|
+
- clientId: \`${auth.clientId || ''}\`
|
|
637
|
+
- useMockAuth: \`${auth.useMockAuth ? 'true' : 'false'}\`
|
|
638
|
+
|
|
639
|
+
## Agent rules
|
|
640
|
+
|
|
641
|
+
- Edit generator templates under CRUDForge \`src/templates/angular/iam/\`, then regenerate — do not treat a testing-area copy as source of truth.
|
|
642
|
+
- Preserve \`// cforge-\` merge zones elsewhere; this pack is overwritten as a unit when IAM is regenerated.
|
|
643
|
+
- Never write Keycloak admin credentials into these components or \`environment.ts\`.
|
|
644
|
+
- Keep new/edit UX consistent with entity \`formMode\` patterns where drawers are used.
|
|
645
|
+
- Nav label group is **User Management**; do not rename to Identity without updating screen IDs + ACL stamps together.
|
|
646
|
+
|
|
647
|
+
## Related docs
|
|
648
|
+
|
|
649
|
+
- App-level: \`AGENT_README.md\`, \`docs/ai/ARCHITECTURE.md\`, \`docs/ai/ROUTES.md\`
|
|
650
|
+
- Generator QA: \`QA/KEYCLOAK-BOOTSTRAP.md\` in the CRUDForge repo
|
|
651
|
+
`;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function registerIamRoutes(outputDir, config, folderName, writeOrPreview) {
|
|
655
|
+
const routesFilePath = path.join(outputDir, config.fuse.routesFile || 'src/app/domains/admin/routes.ts');
|
|
656
|
+
if (!fs.existsSync(routesFilePath)) {
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
let content = fs.readFileSync(routesFilePath, 'utf8');
|
|
661
|
+
const iamRoute = ` {\n path: 'user-management',\n loadChildren: () => import('@/app/core/user-management/user-management.routes'),\n },`;
|
|
662
|
+
|
|
663
|
+
// Migrate older apps/*/identity/user-management imports to core.
|
|
664
|
+
const legacyImport =
|
|
665
|
+
/loadChildren:\s*\(\)\s*=>\s*import\(['"]\.\/modules\/apps\/[^'"]+\/identity\/user-management\/user-management\.routes['"]\)/;
|
|
666
|
+
if (legacyImport.test(content)) {
|
|
667
|
+
content = content.replace(
|
|
668
|
+
legacyImport,
|
|
669
|
+
"loadChildren: () => import('@/app/core/user-management/user-management.routes')"
|
|
670
|
+
);
|
|
671
|
+
writeOrPreview(routesFilePath, content);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (content.includes("path: 'user-management'")) {
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
if (content.includes('// cforge-routes-start')) {
|
|
679
|
+
content = content.replace('// cforge-routes-start', `// cforge-routes-start\n${iamRoute}`);
|
|
680
|
+
} else {
|
|
681
|
+
content = content.replace('children: [', `children: [\n${iamRoute}`);
|
|
682
|
+
}
|
|
683
|
+
writeOrPreview(routesFilePath, content);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function registerIamNavigation(outputDir, config, folderName, writeOrPreview) {
|
|
687
|
+
const navFilePath = path.join(outputDir, config.fuse.navigationFile || 'src/app/domains/admin/layout/data/navigation.ts');
|
|
688
|
+
if (!fs.existsSync(navFilePath)) {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
let content = fs.readFileSync(navFilePath, 'utf8');
|
|
693
|
+
const iam = config.iam || {};
|
|
694
|
+
const children = [` {\n id: 'user-management',\n label: 'Users',\n icon: 'users',\n route: '/admin/user-management'\n }`];
|
|
695
|
+
if (iam.roles !== false) {
|
|
696
|
+
children.push(` {\n id: 'role-management',\n label: 'Roles',\n icon: 'shield',\n route: '/admin/user-management/roles'\n }`);
|
|
697
|
+
}
|
|
698
|
+
if (iam.rolePermissions !== false) {
|
|
699
|
+
children.push(` {\n id: 'role-permissions',\n label: 'Role permissions',\n icon: 'lock',\n route: '/admin/user-management/role-permissions'\n }`);
|
|
700
|
+
}
|
|
701
|
+
const block = ` {\n id: 'identity-group',\n label: 'User Management',\n icon: 'users',\n children: [\n${children.join(',\n')}\n ]\n },`;
|
|
702
|
+
|
|
703
|
+
if (content.includes("id: 'identity-group'")) {
|
|
704
|
+
content = content.replace(
|
|
705
|
+
/id: 'identity-group',\n\s*label: '[^']*',(?:\n\s*description: '[^']*',)?\n\s*icon: '[^']*'/,
|
|
706
|
+
`id: 'identity-group',\n label: 'User Management',\n icon: 'users'`
|
|
707
|
+
);
|
|
708
|
+
writeOrPreview(navFilePath, content);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (content.includes('// cforge-navigation-start')) {
|
|
713
|
+
content = content.replace('// cforge-navigation-start', `// cforge-navigation-start\n${block}`);
|
|
714
|
+
} else if (content.includes('export const NAVIGATION: NavigationItem[] = [')) {
|
|
715
|
+
content = content.replace(
|
|
716
|
+
'export const NAVIGATION: NavigationItem[] = [',
|
|
717
|
+
`export const NAVIGATION: NavigationItem[] = [\n${block}`
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
writeOrPreview(navFilePath, content);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function patchAppConfig(outputDir, writeOrPreview) {
|
|
724
|
+
const filePath = path.join(outputDir, 'src/app/app.config.ts');
|
|
725
|
+
if (!fs.existsSync(filePath)) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
let content = fs.readFileSync(filePath, 'utf8');
|
|
729
|
+
if (!content.includes('authInterceptor')) {
|
|
730
|
+
content = content.replace(
|
|
731
|
+
"import { provideHttpClient, withFetch } from '@angular/common/http';",
|
|
732
|
+
"import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';\nimport { authInterceptor } from '@/app/core/auth';"
|
|
733
|
+
);
|
|
734
|
+
content = content.replace(
|
|
735
|
+
'provideHttpClient(withFetch())',
|
|
736
|
+
'provideHttpClient(withFetch(), withInterceptors([authInterceptor]))'
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
writeOrPreview(filePath, content);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Wire AuthService + DisabledAccountOverlayComponent into Fuse root app.ts (PMB pattern).
|
|
744
|
+
* Overlay only appears when AuthService flips disabledAccountOverlayVisible (flag-gated in env).
|
|
745
|
+
*/
|
|
746
|
+
function patchAppRootOverlay(outputDir, writeOrPreview) {
|
|
747
|
+
const filePath = path.join(outputDir, 'src/app/app.ts');
|
|
748
|
+
if (!fs.existsSync(filePath)) {
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
let content = fs.readFileSync(filePath, 'utf8');
|
|
752
|
+
if (content.includes('DisabledAccountOverlayComponent')) {
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
if (!content.includes("import { Component, inject } from '@angular/core';")) {
|
|
757
|
+
content = content.replace(
|
|
758
|
+
"import { Component } from '@angular/core';",
|
|
759
|
+
"import { Component, inject } from '@angular/core';"
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
if (!content.includes('from "@/app/core/auth"') && !content.includes("from '@/app/core/auth'")) {
|
|
763
|
+
content = content.replace(
|
|
764
|
+
"import { RouterOutlet } from '@angular/router';",
|
|
765
|
+
"import { RouterOutlet } from '@angular/router';\nimport { AuthService, DisabledAccountOverlayComponent } from '@/app/core/auth';\nimport { environment } from '@/environments/environment';"
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
content = content.replace(
|
|
770
|
+
/imports:\s*\[\s*RouterOutlet\s*\]/,
|
|
771
|
+
'imports: [RouterOutlet, DisabledAccountOverlayComponent]'
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
content = content.replace(
|
|
775
|
+
/template:\s*`\s*<router-outlet\s*\/>\s*`/,
|
|
776
|
+
`template: \`
|
|
777
|
+
<div
|
|
778
|
+
class="flex min-h-full w-full flex-auto flex-col"
|
|
779
|
+
[class.pointer-events-none]="authService.disabledAccountOverlayVisible()"
|
|
780
|
+
[class.select-none]="authService.disabledAccountOverlayVisible()"
|
|
781
|
+
>
|
|
782
|
+
<router-outlet />
|
|
783
|
+
</div>
|
|
784
|
+
|
|
785
|
+
@if (authService.disabledAccountOverlayVisible()) {
|
|
786
|
+
<cforge-disabled-account-overlay
|
|
787
|
+
[countdown]="authService.disabledAccountOverlayCountdown()"
|
|
788
|
+
[totalCountdownSeconds]="disabledCountdownSeconds"
|
|
789
|
+
[message]="disabledAccountMessage"
|
|
790
|
+
/>
|
|
791
|
+
}
|
|
792
|
+
\``
|
|
793
|
+
);
|
|
794
|
+
|
|
795
|
+
content = content.replace(
|
|
796
|
+
/export class App \{\s*\}/,
|
|
797
|
+
`export class App {
|
|
798
|
+
readonly authService = inject(AuthService);
|
|
799
|
+
readonly disabledCountdownSeconds =
|
|
800
|
+
Number((environment as { disabledAccountCountdownSeconds?: number }).disabledAccountCountdownSeconds) || 20;
|
|
801
|
+
readonly disabledAccountMessage =
|
|
802
|
+
(environment as { disabledAccountMessage?: string }).disabledAccountMessage ||
|
|
803
|
+
'Your account has been disabled. Please contact your administrator.';
|
|
804
|
+
}`
|
|
805
|
+
);
|
|
806
|
+
|
|
807
|
+
writeOrPreview(filePath, content);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function patchLandingAndNoAccess(outputDir, config, writeOrPreview) {
|
|
811
|
+
const filePath = path.join(outputDir, config.fuse.routesFile || 'src/app/domains/admin/routes.ts');
|
|
812
|
+
if (!fs.existsSync(filePath)) {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
let content = fs.readFileSync(filePath, 'utf8');
|
|
817
|
+
if (!content.includes('LandingGuard')) {
|
|
818
|
+
content = `import { LandingGuard } from '@/app/core/auth';\n${content}`;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
content = content.replace(
|
|
822
|
+
"{ path: '', pathMatch: 'full', redirectTo: '/auth/sign-in' }",
|
|
823
|
+
"{ path: '', pathMatch: 'full', redirectTo: 'welcome' }"
|
|
824
|
+
);
|
|
825
|
+
|
|
826
|
+
if (!content.includes('canActivate: [LandingGuard]')) {
|
|
827
|
+
content = content.replace(
|
|
828
|
+
/path: 'welcome',\n(\s*)loadComponent: \(\) => import\('\.\/modules\/extras\/welcome\/welcome\.component'\)/,
|
|
829
|
+
"path: 'welcome',\n$1canActivate: [LandingGuard],\n$1loadComponent: () => import('./modules/extras/welcome/welcome.component')"
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
if (!content.includes("path: 'no-access'")) {
|
|
834
|
+
content = content.replace(
|
|
835
|
+
/path: 'welcome',[\s\S]*?welcome\.component'\)\n(\s*)\},/,
|
|
836
|
+
(match) =>
|
|
837
|
+
`${match}\n {\n path: 'no-access',\n loadComponent: () => import('./modules/extras/no-access/no-access.component')\n },`
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
writeOrPreview(filePath, content);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function patchAdminRoutesGuard(outputDir, config, writeOrPreview) {
|
|
845
|
+
const filePath = path.join(outputDir, config.fuse.routesFile || 'src/app/domains/admin/routes.ts');
|
|
846
|
+
if (!fs.existsSync(filePath)) {
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
let content = fs.readFileSync(filePath, 'utf8');
|
|
850
|
+
if (!content.includes('AuthGuard')) {
|
|
851
|
+
content = `import { AuthGuard } from '@/app/core/auth';\n${content}`;
|
|
852
|
+
content = content.replace(
|
|
853
|
+
'component: AdminLayout,',
|
|
854
|
+
'component: AdminLayout,\n canActivate: [AuthGuard],\n canActivateChild: [AuthGuard],'
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
writeOrPreview(filePath, content);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function patchAuthRoutesGuard(outputDir, writeOrPreview) {
|
|
861
|
+
const filePath = path.join(outputDir, 'src/app/domains/auth/routes.ts');
|
|
862
|
+
if (!fs.existsSync(filePath)) {
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
let content = fs.readFileSync(filePath, 'utf8');
|
|
866
|
+
if (!content.includes('NoAuthGuard')) {
|
|
867
|
+
content = `import { NoAuthGuard } from '@/app/core/auth';\n${content}`;
|
|
868
|
+
content = content.replace(
|
|
869
|
+
"path: 'sign-in',\n loadComponent: () => import('./features/sign-in/sign-in'),",
|
|
870
|
+
"path: 'sign-in',\n canActivate: [NoAuthGuard],\n loadComponent: () => import('./features/sign-in/sign-in'),"
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
writeOrPreview(filePath, content);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function patchSignInHtml(outputDir, writeOrPreview) {
|
|
877
|
+
const filePath = path.join(outputDir, 'src/app/domains/auth/features/sign-in/sign-in.html');
|
|
878
|
+
if (!fs.existsSync(filePath)) {
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
let html = fs.readFileSync(filePath, 'utf8');
|
|
882
|
+
html = html.replace('<mat-label>Email address</mat-label>', '<mat-label>Username or email</mat-label>');
|
|
883
|
+
if (!html.includes('signInError()')) {
|
|
884
|
+
html = html.replace(
|
|
885
|
+
'<!-- Submit button -->',
|
|
886
|
+
`@if (signInError()) {\n <div class="text-sm text-red-600">{{ signInError() }}</div>\n }\n\n <!-- Submit button -->`
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
// Prefer button click over native form submit (avoids SPA reload with ?ng.form… query params).
|
|
890
|
+
if (html.includes('type="submit"') && !html.includes('(click)="signIn($event)"')) {
|
|
891
|
+
html = html.replace(
|
|
892
|
+
/type="submit"\s*\n(\s*)matButton="filled"\s*\n(\s*)class="mt-6 w-full"\s*\n(\s*)\[disabled\]="signInForm\(\)\.invalid\(\)"/,
|
|
893
|
+
'type="button"\n$1matButton="filled"\n$2class="mt-6 w-full"\n$3[disabled]="signInForm().invalid()"\n$3(click)="signIn($event)"'
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
writeOrPreview(filePath, html);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function patchNavigationFilter(outputDir, writeOrPreview) {
|
|
900
|
+
const filePath = path.join(outputDir, 'src/app/domains/admin/layout/ui/navigation.ts');
|
|
901
|
+
if (!fs.existsSync(filePath) || fs.readFileSync(filePath, 'utf8').includes('AccessControlService')) {
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
let content = fs.readFileSync(filePath, 'utf8');
|
|
906
|
+
content = content.replace(
|
|
907
|
+
"import {\n NAVIGATION,\n NavigationItem,\n} from '@/app/domains/admin/layout/data/navigation';",
|
|
908
|
+
"import {\n NAVIGATION,\n NavigationItem,\n} from '@/app/domains/admin/layout/data/navigation';\nimport { AccessControlService } from '@/app/core/auth';"
|
|
909
|
+
);
|
|
910
|
+
content = content.replace(
|
|
911
|
+
'private router = inject(Router);',
|
|
912
|
+
'private router = inject(Router);\n private accessControl = inject(AccessControlService);'
|
|
913
|
+
);
|
|
914
|
+
content = content.replace(
|
|
915
|
+
'protected navigation = signal<NavigationItem[]>(NAVIGATION);',
|
|
916
|
+
'protected navigation = signal<NavigationItem[]>([]);'
|
|
917
|
+
);
|
|
918
|
+
content = content.replace(
|
|
919
|
+
`constructor() {
|
|
920
|
+
// Expand active route on initial load
|
|
921
|
+
effect(() => {
|
|
922
|
+
const navigationEnd = this.navigationEnd();
|
|
923
|
+
if (!navigationEnd) {
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
this.navigation.set(this.expandActiveRoute(this.navigation()));
|
|
928
|
+
});
|
|
929
|
+
}`,
|
|
930
|
+
`constructor() {
|
|
931
|
+
effect(() => {
|
|
932
|
+
const loaded = this.accessControl.isLoaded() || this.accessControl.isUserAdmin();
|
|
933
|
+
const items = loaded ? this.filterNavigation(NAVIGATION) : [];
|
|
934
|
+
const navigationEnd = this.navigationEnd();
|
|
935
|
+
this.navigation.set(navigationEnd ? this.expandActiveRoute(items) : items);
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
private filterNavigation(items: NavigationItem[]): NavigationItem[] {
|
|
940
|
+
const next: NavigationItem[] = [];
|
|
941
|
+
for (const item of items) {
|
|
942
|
+
const children = item.children?.length ? this.filterNavigation(item.children) : item.children;
|
|
943
|
+
if (item.route && item.id && !this.accessControl.hasScreenAccess(item.id)) {
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
if (children && children.length === 0 && !item.route) {
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
next.push({ ...item, children });
|
|
950
|
+
}
|
|
951
|
+
return next;
|
|
952
|
+
}`
|
|
953
|
+
);
|
|
954
|
+
writeOrPreview(filePath, content);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
async function renderTemplate(templatePath, locals) {
|
|
958
|
+
const template = await fs.readFile(templatePath, 'utf8');
|
|
959
|
+
return ejs.render(template, locals, { filename: templatePath });
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
module.exports = {
|
|
963
|
+
applyAuthPack,
|
|
964
|
+
isKeycloakAuthEnabled,
|
|
965
|
+
};
|